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,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,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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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.
|
|
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,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
|
+
}
|