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,41 @@
1
+ const DEFAULT_TTL_MS = 5 * 60 * 1000; // 5 minutes
2
+ const DEFAULT_POLL_INTERVAL_MS = 5 * 1000; // 5 seconds
3
+ const MIN_POLL_INTERVAL_MS = 1000; // 1 second minimum to prevent tight loops
4
+ /**
5
+ * Parses a positive integer from a string, returning the default if invalid.
6
+ */
7
+ function parsePositiveInt(value, defaultValue) {
8
+ if (!value)
9
+ return defaultValue;
10
+ const parsed = parseInt(value, 10);
11
+ return Number.isNaN(parsed) || parsed < 0 ? defaultValue : parsed;
12
+ }
13
+ /**
14
+ * Reads elicitation configuration from environment variables.
15
+ *
16
+ * Environment variables:
17
+ * ELICITATION_ENABLED - "true" (default) or "false"
18
+ * ELICITATION_REQUEST_URL - POST endpoint for HTTP fallback
19
+ * ELICITATION_POLL_URL - GET endpoint for HTTP fallback polling
20
+ * ELICITATION_TTL_MS - Request TTL in milliseconds (default: 300000)
21
+ * ELICITATION_POLL_INTERVAL_MS - Poll interval in milliseconds (default: 5000, min: 1000)
22
+ * ELICITATION_SESSION_ID - Session identifier for HTTP fallback `_meta`
23
+ * ELICITATION_PREFER_HTTP_FALLBACK - "true" forces HTTP fallback over native elicitation
24
+ * when both are available. Default: "false".
25
+ */
26
+ export function readElicitationConfig(env = process.env) {
27
+ const enabledRaw = env.ELICITATION_ENABLED;
28
+ const enabled = enabledRaw === undefined ? true : enabledRaw.toLowerCase() !== 'false';
29
+ const preferHttpFallbackRaw = env.ELICITATION_PREFER_HTTP_FALLBACK;
30
+ const preferHttpFallback = preferHttpFallbackRaw !== undefined && preferHttpFallbackRaw.toLowerCase() === 'true';
31
+ const pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, parsePositiveInt(env.ELICITATION_POLL_INTERVAL_MS, DEFAULT_POLL_INTERVAL_MS));
32
+ return {
33
+ enabled,
34
+ requestUrl: env.ELICITATION_REQUEST_URL,
35
+ pollUrl: env.ELICITATION_POLL_URL,
36
+ ttlMs: parsePositiveInt(env.ELICITATION_TTL_MS, DEFAULT_TTL_MS),
37
+ pollIntervalMs,
38
+ sessionId: env.ELICITATION_SESSION_ID,
39
+ preferHttpFallback,
40
+ };
41
+ }
@@ -0,0 +1,24 @@
1
+ import type { ElicitationConfig, ElicitationRequestedSchema, ElicitationResult, RequestConfirmationOptions } from './types.js';
2
+ /**
3
+ * Requests user confirmation through the best available mechanism.
4
+ *
5
+ * Decision tree (default):
6
+ * 1. If elicitation is disabled (`ELICITATION_ENABLED=false`), returns `accept` immediately.
7
+ * 2. If the client supports native elicitation, uses `server.elicitInput()`.
8
+ * 3. If HTTP fallback URLs are configured, posts to the external endpoint and polls.
9
+ * 4. Otherwise, throws an error indicating no elicitation mechanism is available.
10
+ *
11
+ * When `cfg.preferHttpFallback` is true (set via `ELICITATION_PREFER_HTTP_FALLBACK=true`)
12
+ * AND both fallback URLs are configured, tier 3 runs before tier 2. This is intended for
13
+ * headless agent runtimes that falsely advertise elicitation capability but cannot actually
14
+ * surface the prompt to a user.
15
+ *
16
+ * @param options - Configuration for the confirmation request.
17
+ * @param config - Elicitation config (defaults to reading from env vars).
18
+ * @returns The user's response.
19
+ */
20
+ export declare function requestConfirmation(options: RequestConfirmationOptions, config?: ElicitationConfig): Promise<ElicitationResult>;
21
+ /**
22
+ * Creates a simple boolean confirmation schema for common "are you sure?" prompts.
23
+ */
24
+ export declare function createConfirmationSchema(title?: string, description?: string): ElicitationRequestedSchema;
@@ -0,0 +1,175 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { readElicitationConfig } from './config.js';
3
+ /**
4
+ * The set of action values recognized by the elicitation protocol.
5
+ * Includes 'pending' for completeness, though it is filtered before
6
+ * reaching the validation check in pollElicitationStatus.
7
+ */
8
+ const VALID_ELICITATION_ACTIONS = new Set(['pending', 'accept', 'decline', 'cancel', 'expired']);
9
+ /**
10
+ * Checks whether the connected client supports native form elicitation.
11
+ */
12
+ function clientSupportsElicitation(server) {
13
+ const caps = server.getClientCapabilities();
14
+ if (!caps?.elicitation) {
15
+ return false;
16
+ }
17
+ // If elicitation is declared at all (even empty {}), form mode is supported
18
+ // per the MCP spec's backward compatibility rules.
19
+ return true;
20
+ }
21
+ /**
22
+ * Attempts native elicitation via the MCP SDK's `server.elicitInput()`.
23
+ */
24
+ async function nativeElicit(server, message, requestedSchema) {
25
+ const params = {
26
+ mode: 'form',
27
+ message,
28
+ requestedSchema,
29
+ };
30
+ const result = await server.elicitInput(params);
31
+ // Fail-safe: validate the action even from native elicitation.
32
+ // The TypeScript type says 'accept' | 'decline' | 'cancel', but at runtime
33
+ // the MCP client could return any string over the wire.
34
+ if (!VALID_ELICITATION_ACTIONS.has(result.action)) {
35
+ console.warn(`[elicitation] Unrecognized native elicitation action "${result.action}". ` +
36
+ `Treating as "decline" (fail-safe).`);
37
+ return { action: 'decline' };
38
+ }
39
+ return {
40
+ action: result.action,
41
+ content: result.content ?? undefined,
42
+ };
43
+ }
44
+ /**
45
+ * Posts an elicitation request to the HTTP fallback endpoint.
46
+ */
47
+ async function postElicitationRequest(config, message, requestedSchema, meta) {
48
+ const response = await fetch(config.requestUrl, {
49
+ method: 'POST',
50
+ headers: { 'Content-Type': 'application/json' },
51
+ body: JSON.stringify({
52
+ mode: 'form',
53
+ message,
54
+ requestedSchema,
55
+ _meta: meta,
56
+ }),
57
+ });
58
+ if (!response.ok) {
59
+ const body = await response.text().catch(() => '');
60
+ throw new Error(`Elicitation POST failed: ${response.status} ${response.statusText}${body ? ` - ${body}` : ''}`);
61
+ }
62
+ const data = (await response.json());
63
+ return data;
64
+ }
65
+ /**
66
+ * Polls the HTTP fallback endpoint until the request is resolved or expires.
67
+ */
68
+ async function pollElicitationStatus(config, requestId, expiresAt) {
69
+ const pollUrl = config.pollUrl.endsWith('/')
70
+ ? `${config.pollUrl}${requestId}`
71
+ : `${config.pollUrl}/${requestId}`;
72
+ while (Date.now() < expiresAt) {
73
+ const response = await fetch(pollUrl, {
74
+ method: 'GET',
75
+ headers: { 'Content-Type': 'application/json' },
76
+ });
77
+ if (!response.ok) {
78
+ const body = await response.text().catch(() => '');
79
+ throw new Error(`Elicitation poll failed: ${response.status} ${response.statusText}${body ? ` - ${body}` : ''}`);
80
+ }
81
+ const data = (await response.json());
82
+ if (data.action !== 'pending') {
83
+ // Fail-safe: only allow recognized action values through.
84
+ // Unrecognized actions are treated as 'decline' to prevent
85
+ // unintended execution of protected operations.
86
+ if (!VALID_ELICITATION_ACTIONS.has(data.action)) {
87
+ console.warn(`[elicitation] Unrecognized poll action "${data.action}" for request ${requestId}. ` +
88
+ `Treating as "decline" (fail-safe).`);
89
+ return { action: 'decline' };
90
+ }
91
+ return {
92
+ action: data.action,
93
+ content: data.content ?? undefined,
94
+ };
95
+ }
96
+ // Wait before polling again
97
+ await new Promise((resolve) => setTimeout(resolve, config.pollIntervalMs));
98
+ }
99
+ return { action: 'expired' };
100
+ }
101
+ /**
102
+ * Runs the HTTP fallback flow: POST a request, then poll until resolved or expired.
103
+ */
104
+ async function httpFallbackElicit(cfg, options) {
105
+ const clientRequestId = randomUUID();
106
+ const expiresAt = Date.now() + cfg.ttlMs;
107
+ const meta = {
108
+ 'com.pulsemcp/request-id': clientRequestId,
109
+ 'com.pulsemcp/expires-at': new Date(expiresAt).toISOString(),
110
+ ...(cfg.sessionId && { 'com.pulsemcp/session-id': cfg.sessionId }),
111
+ ...options.meta,
112
+ };
113
+ const postResponse = await postElicitationRequest(cfg, options.message, options.requestedSchema, meta);
114
+ // Use the server-provided requestId if available, otherwise fall back to the client-generated one
115
+ const requestId = postResponse.requestId || clientRequestId;
116
+ return pollElicitationStatus(cfg, requestId, expiresAt);
117
+ }
118
+ /**
119
+ * Requests user confirmation through the best available mechanism.
120
+ *
121
+ * Decision tree (default):
122
+ * 1. If elicitation is disabled (`ELICITATION_ENABLED=false`), returns `accept` immediately.
123
+ * 2. If the client supports native elicitation, uses `server.elicitInput()`.
124
+ * 3. If HTTP fallback URLs are configured, posts to the external endpoint and polls.
125
+ * 4. Otherwise, throws an error indicating no elicitation mechanism is available.
126
+ *
127
+ * When `cfg.preferHttpFallback` is true (set via `ELICITATION_PREFER_HTTP_FALLBACK=true`)
128
+ * AND both fallback URLs are configured, tier 3 runs before tier 2. This is intended for
129
+ * headless agent runtimes that falsely advertise elicitation capability but cannot actually
130
+ * surface the prompt to a user.
131
+ *
132
+ * @param options - Configuration for the confirmation request.
133
+ * @param config - Elicitation config (defaults to reading from env vars).
134
+ * @returns The user's response.
135
+ */
136
+ export async function requestConfirmation(options, config) {
137
+ const cfg = config ?? readElicitationConfig();
138
+ // Tier 1: Disabled — skip confirmation entirely
139
+ if (!cfg.enabled) {
140
+ return { action: 'accept' };
141
+ }
142
+ const httpFallbackAvailable = Boolean(cfg.requestUrl && cfg.pollUrl);
143
+ // Opt-in: prefer HTTP fallback over native elicitation when both are available.
144
+ if (cfg.preferHttpFallback && httpFallbackAvailable) {
145
+ return httpFallbackElicit(cfg, options);
146
+ }
147
+ // Tier 2: Native elicitation
148
+ if (clientSupportsElicitation(options.server)) {
149
+ return nativeElicit(options.server, options.message, options.requestedSchema);
150
+ }
151
+ // Tier 3: HTTP fallback
152
+ if (httpFallbackAvailable) {
153
+ return httpFallbackElicit(cfg, options);
154
+ }
155
+ // Tier 4: No mechanism available
156
+ throw new Error('Elicitation is enabled but no mechanism is available. ' +
157
+ 'Either the client must support native elicitation, or ' +
158
+ 'ELICITATION_REQUEST_URL and ELICITATION_POLL_URL must be configured for HTTP fallback.');
159
+ }
160
+ /**
161
+ * Creates a simple boolean confirmation schema for common "are you sure?" prompts.
162
+ */
163
+ export function createConfirmationSchema(title = 'Confirm', description) {
164
+ return {
165
+ type: 'object',
166
+ properties: {
167
+ confirm: {
168
+ type: 'boolean',
169
+ title,
170
+ ...(description ? { description } : {}),
171
+ },
172
+ },
173
+ required: ['confirm'],
174
+ };
175
+ }
@@ -0,0 +1,3 @@
1
+ export { requestConfirmation, createConfirmationSchema } from './elicitation.js';
2
+ export { readElicitationConfig } from './config.js';
3
+ export type { ElicitationConfig, ElicitationFieldSchema, ElicitationMeta, ElicitationPollResponse, ElicitationPostResponse, ElicitationRequestedSchema, ElicitationResult, MCPServerLike, RequestConfirmationOptions, } from './types.js';
@@ -0,0 +1,2 @@
1
+ export { requestConfirmation, createConfirmationSchema } from './elicitation.js';
2
+ export { readElicitationConfig } from './config.js';
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Minimal interface for the MCP server, avoiding direct dependency on
3
+ * @modelcontextprotocol/sdk Server type to prevent cross-package type
4
+ * mismatches in monorepo setups with multiple SDK installations.
5
+ */
6
+ export interface MCPServerLike {
7
+ getClientCapabilities(): {
8
+ elicitation?: unknown;
9
+ } | undefined;
10
+ elicitInput(params: unknown): Promise<{
11
+ action: 'accept' | 'decline' | 'cancel';
12
+ content?: Record<string, string | number | boolean | string[]>;
13
+ }>;
14
+ }
15
+ /**
16
+ * Vendor metadata for PulseMCP elicitation requests.
17
+ * Uses reverse-DNS prefix `com.pulsemcp/` per MCP spec conventions.
18
+ */
19
+ export interface ElicitationMeta {
20
+ 'com.pulsemcp/request-id'?: string;
21
+ 'com.pulsemcp/tool-name'?: string;
22
+ 'com.pulsemcp/context'?: string;
23
+ 'com.pulsemcp/session-id'?: string;
24
+ 'com.pulsemcp/expires-at'?: string;
25
+ }
26
+ /**
27
+ * Schema for a single field in a form elicitation request.
28
+ * Maps to PrimitiveSchemaDefinition in the MCP spec.
29
+ */
30
+ export interface ElicitationFieldSchema {
31
+ type: 'string' | 'number' | 'integer' | 'boolean';
32
+ title?: string;
33
+ description?: string;
34
+ default?: string | number | boolean;
35
+ minLength?: number;
36
+ maxLength?: number;
37
+ format?: 'email' | 'uri' | 'date' | 'date-time';
38
+ enum?: string[];
39
+ minimum?: number;
40
+ maximum?: number;
41
+ }
42
+ /**
43
+ * Schema for the form presented to users during elicitation.
44
+ */
45
+ export interface ElicitationRequestedSchema {
46
+ type: 'object';
47
+ properties: Record<string, ElicitationFieldSchema>;
48
+ required?: string[];
49
+ }
50
+ /**
51
+ * Configuration for the elicitation system.
52
+ * Read from environment variables at initialization.
53
+ */
54
+ export interface ElicitationConfig {
55
+ /** Whether elicitation is enabled at all. Default: true (reads from ELICITATION_ENABLED env var). */
56
+ enabled: boolean;
57
+ /** POST endpoint for creating approval requests (HTTP fallback). */
58
+ requestUrl?: string;
59
+ /** Base URL for polling approval status (HTTP fallback). */
60
+ pollUrl?: string;
61
+ /** TTL for elicitation requests in milliseconds. Default: 5 minutes. */
62
+ ttlMs: number;
63
+ /** Poll interval in milliseconds. Default: 5 seconds. */
64
+ pollIntervalMs: number;
65
+ /** Session identifier included as `com.pulsemcp/session-id` in `_meta` of HTTP fallback requests. */
66
+ sessionId?: string;
67
+ /**
68
+ * When true, prefer HTTP fallback (Tier 3) over native elicitation (Tier 2)
69
+ * when both are available. Default: false.
70
+ *
71
+ * Useful for headless agent runtimes (e.g., Claude Code under Agent Orchestrator)
72
+ * that advertise the `elicitation` client capability but have no real interactive
73
+ * user — native `elicitInput()` calls auto-cancel without ever surfacing a prompt.
74
+ * Forcing the HTTP fallback routes the request to an external approval UI instead.
75
+ */
76
+ preferHttpFallback?: boolean;
77
+ }
78
+ /**
79
+ * The result of an elicitation request.
80
+ */
81
+ export interface ElicitationResult {
82
+ action: 'accept' | 'decline' | 'cancel' | 'expired';
83
+ content?: Record<string, string | number | boolean | string[]>;
84
+ }
85
+ /**
86
+ * HTTP fallback response from the polling endpoint.
87
+ */
88
+ export interface ElicitationPollResponse {
89
+ action: 'pending' | 'accept' | 'decline' | 'cancel' | 'expired';
90
+ content?: Record<string, string | number | boolean | string[]> | null;
91
+ _meta?: {
92
+ 'com.pulsemcp/request-id'?: string;
93
+ 'com.pulsemcp/responded-at'?: string | null;
94
+ };
95
+ }
96
+ /**
97
+ * HTTP fallback response from the POST request endpoint.
98
+ */
99
+ export interface ElicitationPostResponse {
100
+ requestId: string;
101
+ }
102
+ /**
103
+ * Options passed to the requestConfirmation function.
104
+ */
105
+ export interface RequestConfirmationOptions {
106
+ /** The MCP server instance (needed for native elicitation). */
107
+ server: MCPServerLike;
108
+ /** Human-readable message explaining what needs confirmation. */
109
+ message: string;
110
+ /** Schema for the form fields to present. */
111
+ requestedSchema: ElicitationRequestedSchema;
112
+ /** Optional vendor metadata. */
113
+ meta?: ElicitationMeta;
114
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@pulsemcp/mcp-elicitation",
3
+ "version": "1.1.0",
4
+ "description": "Elicitation support library for PulseMCP MCP servers - provides native elicitation with HTTP fallback",
5
+ "type": "module",
6
+ "main": "build/index.js",
7
+ "types": "build/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "test": "vitest run",
11
+ "lint": "eslint . --ext .ts,.tsx",
12
+ "lint:fix": "eslint . --ext .ts,.tsx --fix",
13
+ "format": "prettier --write .",
14
+ "format:check": "prettier --check ."
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "elicitation",
19
+ "pulsemcp"
20
+ ],
21
+ "author": "PulseMCP",
22
+ "license": "MIT",
23
+ "devDependencies": {
24
+ "@types/node": "^24.10.12",
25
+ "typescript": "^5.7.3",
26
+ "vitest": "^3.2.3"
27
+ }
28
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulsemcp-cms-admin-mcp-server",
3
- "version": "0.9.17",
3
+ "version": "0.9.26",
4
4
  "description": "Local implementation of PulseMCP CMS Admin MCP server",
5
5
  "mcpName": "com.pulsemcp.servers/pulsemcp-cms-admin",
6
6
  "main": "build/index.js",
@@ -13,8 +13,13 @@
13
13
  "build/**/*.d.ts",
14
14
  "shared/**/*.js",
15
15
  "shared/**/*.d.ts",
16
+ "node_modules/@pulsemcp/mcp-elicitation/**/*.js",
17
+ "node_modules/@pulsemcp/mcp-elicitation/package.json",
16
18
  "README.md"
17
19
  ],
20
+ "bundledDependencies": [
21
+ "@pulsemcp/mcp-elicitation"
22
+ ],
18
23
  "scripts": {
19
24
  "build": "tsc && npm run build:integration",
20
25
  "build:integration": "tsc -p tsconfig.integration.json",
@@ -31,6 +36,7 @@
31
36
  },
32
37
  "dependencies": {
33
38
  "@modelcontextprotocol/sdk": "^1.29.0",
39
+ "@pulsemcp/mcp-elicitation": "file:../../../libs/elicitation",
34
40
  "zod": "^3.24.1"
35
41
  },
36
42
  "devDependencies": {
@@ -0,0 +1,61 @@
1
+ import { type ElicitationConfig } from '@pulsemcp/mcp-elicitation';
2
+ /**
3
+ * pulsemcp-cms-admin elicitation configuration.
4
+ *
5
+ * Layers:
6
+ * 1. Base elicitation config from @pulsemcp/mcp-elicitation
7
+ * 2. DANGEROUSLY_SKIP_ELICITATIONS override (must be explicitly "true" to bypass all elicitation)
8
+ * 3. Per-action override: PULSEMCP_CMS_ADMIN_ELICITATION_DESTRUCTIVE
9
+ *
10
+ * Destructive elicitation gates the `tenants_destructive` tool group only
11
+ * (delete_tenant, delete_api_key). All other write tools in the server are
12
+ * not gated by elicitation.
13
+ */
14
+ export interface CmsAdminElicitationConfig {
15
+ /** Base elicitation config from the shared library */
16
+ base: ElicitationConfig;
17
+ /** Whether to elicit confirmation for destructive operations (delete_tenant, delete_api_key) */
18
+ destructiveElicitationEnabled: boolean;
19
+ }
20
+ /**
21
+ * Check whether DANGEROUSLY_SKIP_ELICITATIONS is explicitly set to "true".
22
+ */
23
+ export declare function isDangerouslySkipElicitations(env?: Record<string, string | undefined>): boolean;
24
+ /**
25
+ * Check whether HTTP fallback elicitation URLs are configured.
26
+ */
27
+ export declare function hasHttpElicitationFallback(env?: Record<string, string | undefined>): boolean;
28
+ /**
29
+ * Read the full pulsemcp-cms-admin elicitation configuration from environment variables.
30
+ *
31
+ * Environment variables:
32
+ * DANGEROUSLY_SKIP_ELICITATIONS - Must be explicitly "true" to bypass all elicitation.
33
+ * PULSEMCP_CMS_ADMIN_ELICITATION_DESTRUCTIVE - Override for destructive operations (default: follows base enabled state)
34
+ *
35
+ * Plus all standard elicitation env vars (ELICITATION_REQUEST_URL, etc.)
36
+ */
37
+ export declare function readCmsAdminElicitationConfig(env?: Record<string, string | undefined>): CmsAdminElicitationConfig;
38
+ /**
39
+ * Result of the elicitation safety check.
40
+ */
41
+ export type ElicitationSafetyResult = {
42
+ safe: true;
43
+ reason: 'dangerously_skip' | 'http_fallback';
44
+ } | {
45
+ safe: false;
46
+ reason: 'no_elicitation_configured';
47
+ };
48
+ /**
49
+ * Check whether the elicitation configuration is safe to start the server when
50
+ * destructive tools are enabled.
51
+ *
52
+ * The server is "safe" if either:
53
+ * - DANGEROUSLY_SKIP_ELICITATIONS=true is explicitly set (operator opted out), or
54
+ * - HTTP fallback URLs are configured (so elicitation will be reachable).
55
+ *
56
+ * Native MCP elicitation support cannot be detected at startup (it requires an
57
+ * active client connection), so callers relying solely on native elicitation
58
+ * should also configure HTTP fallback URLs.
59
+ */
60
+ export declare function checkElicitationSafety(env?: Record<string, string | undefined>): ElicitationSafetyResult;
61
+ //# sourceMappingURL=elicitation-config.d.ts.map
@@ -0,0 +1,69 @@
1
+ import { readElicitationConfig } from '@pulsemcp/mcp-elicitation';
2
+ /**
3
+ * Parse a boolean environment variable with a default.
4
+ * Accepts "true"/"false" (case-insensitive). Unset = default.
5
+ */
6
+ function parseBooleanEnv(value, defaultValue) {
7
+ if (value === undefined)
8
+ return defaultValue;
9
+ return value.toLowerCase() !== 'false';
10
+ }
11
+ /**
12
+ * Check whether DANGEROUSLY_SKIP_ELICITATIONS is explicitly set to "true".
13
+ */
14
+ export function isDangerouslySkipElicitations(env = process.env) {
15
+ return env.DANGEROUSLY_SKIP_ELICITATIONS?.toLowerCase() === 'true';
16
+ }
17
+ /**
18
+ * Check whether HTTP fallback elicitation URLs are configured.
19
+ */
20
+ export function hasHttpElicitationFallback(env = process.env) {
21
+ return !!(env.ELICITATION_REQUEST_URL?.trim() && env.ELICITATION_POLL_URL?.trim());
22
+ }
23
+ /**
24
+ * Read the full pulsemcp-cms-admin elicitation configuration from environment variables.
25
+ *
26
+ * Environment variables:
27
+ * DANGEROUSLY_SKIP_ELICITATIONS - Must be explicitly "true" to bypass all elicitation.
28
+ * PULSEMCP_CMS_ADMIN_ELICITATION_DESTRUCTIVE - Override for destructive operations (default: follows base enabled state)
29
+ *
30
+ * Plus all standard elicitation env vars (ELICITATION_REQUEST_URL, etc.)
31
+ */
32
+ export function readCmsAdminElicitationConfig(env = process.env) {
33
+ // Map DANGEROUSLY_SKIP_ELICITATIONS to the base library's enabled flag.
34
+ // The cms-admin server does not use ELICITATION_ENABLED — the only way to disable
35
+ // destructive elicitation is via DANGEROUSLY_SKIP_ELICITATIONS=true.
36
+ const dangerouslySkip = isDangerouslySkipElicitations(env);
37
+ const base = readElicitationConfig({
38
+ ...env,
39
+ ELICITATION_ENABLED: dangerouslySkip ? 'false' : 'true',
40
+ });
41
+ const destructiveElicitationEnabled = base.enabled
42
+ ? parseBooleanEnv(env.PULSEMCP_CMS_ADMIN_ELICITATION_DESTRUCTIVE, true)
43
+ : false;
44
+ return {
45
+ base,
46
+ destructiveElicitationEnabled,
47
+ };
48
+ }
49
+ /**
50
+ * Check whether the elicitation configuration is safe to start the server when
51
+ * destructive tools are enabled.
52
+ *
53
+ * The server is "safe" if either:
54
+ * - DANGEROUSLY_SKIP_ELICITATIONS=true is explicitly set (operator opted out), or
55
+ * - HTTP fallback URLs are configured (so elicitation will be reachable).
56
+ *
57
+ * Native MCP elicitation support cannot be detected at startup (it requires an
58
+ * active client connection), so callers relying solely on native elicitation
59
+ * should also configure HTTP fallback URLs.
60
+ */
61
+ export function checkElicitationSafety(env = process.env) {
62
+ if (isDangerouslySkipElicitations(env)) {
63
+ return { safe: true, reason: 'dangerously_skip' };
64
+ }
65
+ if (hasHttpElicitationFallback(env)) {
66
+ return { safe: true, reason: 'http_fallback' };
67
+ }
68
+ return { safe: false, reason: 'no_elicitation_configured' };
69
+ }
@@ -0,0 +1,3 @@
1
+ import type { BulkUpdateTenantServersParams, BulkUpdateTenantServersResponse } from '../../types.js';
2
+ export declare function bulkUpdateTenantServers(apiKey: string, baseUrl: string, idOrSlug: number | string, params: BulkUpdateTenantServersParams): Promise<BulkUpdateTenantServersResponse>;
3
+ //# sourceMappingURL=bulk-update-tenant-servers.d.ts.map
@@ -0,0 +1,41 @@
1
+ import { adminFetch } from './admin-fetch.js';
2
+ export async function bulkUpdateTenantServers(apiKey, baseUrl, idOrSlug, params) {
3
+ const url = new URL(`/api/tenants/${idOrSlug}/servers/bulk_update`, baseUrl);
4
+ const body = {};
5
+ if (params.add_server_identifiers && params.add_server_identifiers.length > 0) {
6
+ body.add_server_identifiers = params.add_server_identifiers;
7
+ }
8
+ if (params.remove_server_identifiers && params.remove_server_identifiers.length > 0) {
9
+ body.remove_server_identifiers = params.remove_server_identifiers;
10
+ }
11
+ if (params.restore_association_ids && params.restore_association_ids.length > 0) {
12
+ body.restore_association_ids = params.restore_association_ids;
13
+ }
14
+ const response = await adminFetch(url.toString(), {
15
+ method: 'POST',
16
+ headers: {
17
+ 'X-API-Key': apiKey,
18
+ 'Content-Type': 'application/json',
19
+ Accept: 'application/json',
20
+ },
21
+ body: JSON.stringify(body),
22
+ });
23
+ if (!response.ok) {
24
+ if (response.status === 401) {
25
+ throw new Error('Invalid API key');
26
+ }
27
+ if (response.status === 403) {
28
+ throw new Error('User lacks write privileges');
29
+ }
30
+ if (response.status === 404) {
31
+ throw new Error(`Tenant with ID/slug ${idOrSlug} not found`);
32
+ }
33
+ if (response.status === 422) {
34
+ const errorData = (await response.json());
35
+ const detail = errorData.message || errorData.error_code || 'Unknown error';
36
+ throw new Error(`Bulk update failed: ${detail}`);
37
+ }
38
+ throw new Error(`Failed to bulk-update tenant servers: ${response.status} ${response.statusText}`);
39
+ }
40
+ return (await response.json());
41
+ }
@@ -88,6 +88,13 @@ export async function createMCPImplementation(apiKey, baseUrl, params) {
88
88
  if (params.internal_notes !== undefined) {
89
89
  formData.append('mcp_implementation[internal_notes]', params.internal_notes);
90
90
  }
91
+ // Owner tenant linking. Slug wins over id on the Rails side when both are sent.
92
+ if (params.owner_tenant_slug !== undefined) {
93
+ formData.append('mcp_implementation[owner_tenant_slug]', params.owner_tenant_slug === null ? '' : params.owner_tenant_slug);
94
+ }
95
+ if (params.owner_tenant_id !== undefined) {
96
+ formData.append('mcp_implementation[owner_tenant_id]', params.owner_tenant_id === null ? '' : params.owner_tenant_id.toString());
97
+ }
91
98
  // Remote endpoints
92
99
  // Rails expects nested attributes to use the _attributes suffix for has_many associations
93
100
  if (params.remote !== undefined) {
@@ -0,0 +1,3 @@
1
+ import type { DeleteApiKeyResponse } from '../../types.js';
2
+ export declare function deleteApiKey(apiKey: string, baseUrl: string, id: number): Promise<DeleteApiKeyResponse>;
3
+ //# sourceMappingURL=delete-api-key.d.ts.map
@@ -0,0 +1,25 @@
1
+ import { adminFetch } from './admin-fetch.js';
2
+ export async function deleteApiKey(apiKey, baseUrl, id) {
3
+ const url = new URL(`/api/api_keys/${id}`, baseUrl);
4
+ const response = await adminFetch(url.toString(), {
5
+ method: 'DELETE',
6
+ headers: {
7
+ 'X-API-Key': apiKey,
8
+ Accept: 'application/json',
9
+ },
10
+ });
11
+ if (!response.ok) {
12
+ if (response.status === 401) {
13
+ throw new Error('Invalid API key');
14
+ }
15
+ if (response.status === 403) {
16
+ throw new Error('User lacks write privileges');
17
+ }
18
+ if (response.status === 422) {
19
+ const errorData = (await response.json().catch(() => ({})));
20
+ throw new Error(`Cannot delete API key: ${errorData.errors?.join(', ') || 'validation failed'}`);
21
+ }
22
+ throw new Error(`Failed to delete API key: ${response.status} ${response.statusText}`);
23
+ }
24
+ return (await response.json());
25
+ }
@@ -0,0 +1,3 @@
1
+ import type { DeleteTenantParams, DeleteTenantResponse } from '../../types.js';
2
+ export declare function deleteTenant(apiKey: string, baseUrl: string, params: DeleteTenantParams): Promise<DeleteTenantResponse>;
3
+ //# sourceMappingURL=delete-tenant.d.ts.map