@sendinel/mcp-server 1.0.159 → 1.0.160
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/build/lib/action-approvals.d.ts +8 -0
- package/build/lib/action-approvals.js +25 -0
- package/build/lib/approval-capabilities.d.ts +3 -1
- package/build/lib/approval-capabilities.js +17 -2
- package/build/lib/approval-elicitation-evidence.d.ts +34 -0
- package/build/lib/approval-elicitation-evidence.js +27 -0
- package/build/lib/approval-elicitation-relay.d.ts +3 -1
- package/build/lib/approval-elicitation-relay.js +33 -1
- package/build/lib/approval-elicitation.d.ts +2 -0
- package/build/lib/approval-elicitation.js +10 -2
- package/build/lib/approval-human-callback.d.ts +2 -0
- package/build/lib/approval-human-callback.js +1 -1
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DecisionEvidence } from './approval-elicitation-evidence.js';
|
|
1
2
|
export { isApprovalGatedAction } from './approval-actions.js';
|
|
2
3
|
/**
|
|
3
4
|
* Action approval tiers:
|
|
@@ -84,6 +85,13 @@ export declare function checkApproval(approvalId: string, options?: {
|
|
|
84
85
|
consume?: boolean;
|
|
85
86
|
action?: string;
|
|
86
87
|
params?: Record<string, unknown>;
|
|
88
|
+
}): Promise<Awaited<ReturnType<typeof checkApprovalState>> & {
|
|
89
|
+
decision_evidence?: DecisionEvidence;
|
|
90
|
+
}>;
|
|
91
|
+
declare function checkApprovalState(approvalId: string, options?: {
|
|
92
|
+
consume?: boolean;
|
|
93
|
+
action?: string;
|
|
94
|
+
params?: Record<string, unknown>;
|
|
87
95
|
}): Promise<{
|
|
88
96
|
approved: boolean;
|
|
89
97
|
status: string;
|
|
@@ -371,6 +371,31 @@ function captureSnapshot(action, params) {
|
|
|
371
371
|
* Returns the approval if approved, null if still pending, throws if rejected/expired.
|
|
372
372
|
*/
|
|
373
373
|
export async function checkApproval(approvalId, options = {}) {
|
|
374
|
+
const result = await checkApprovalState(approvalId, options);
|
|
375
|
+
if (options.consume || result.status === 'not_found')
|
|
376
|
+
return result;
|
|
377
|
+
try {
|
|
378
|
+
const source = approvalStore();
|
|
379
|
+
const client = typeof source.schema === 'function' ? source.schema('email') : source;
|
|
380
|
+
const { data: approval, error } = await client.from('action_approvals')
|
|
381
|
+
.select('decided_via, decided_by').eq('id', approvalId).eq('project_id', getProjectId()).maybeSingle();
|
|
382
|
+
if (error || approval?.decided_via !== 'mcp_elicitation' || !approval.decided_by)
|
|
383
|
+
return result;
|
|
384
|
+
const { data: rows, error: auditError } = await client.from('audit_log').select('details')
|
|
385
|
+
.eq('project_id', getProjectId()).eq('resource_type', 'approval').eq('resource_id', approvalId)
|
|
386
|
+
.eq('actor_id', approval.decided_by).in('action', ['approval.elicitation_answered', 'approval.submitted']).limit(10);
|
|
387
|
+
if (auditError)
|
|
388
|
+
return result;
|
|
389
|
+
const evidence = rows?.map((row) => row.details).find((details) => details?.channel === 'mcp_elicitation' && Number.isInteger(details.answer_latency_ms) &&
|
|
390
|
+
details.answer_latency_ms >= 0 && typeof details.auto_accept_suspected === 'boolean');
|
|
391
|
+
return evidence ? { ...result, decision_evidence: { answer_latency_ms: evidence.answer_latency_ms,
|
|
392
|
+
auto_accept_suspected: evidence.auto_accept_suspected, channel: 'mcp_elicitation' } } : result;
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
return result;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async function checkApprovalState(approvalId, options = {}) {
|
|
374
399
|
const applied = registeredApproval.getStore();
|
|
375
400
|
if (options.consume && applied?.phase === 'approved' && applied.id === approvalId && applied.action === options.action && applied.projectId === getProjectId()) {
|
|
376
401
|
return { approved: true, status: 'approved', decided_by: applied.decidedBy, ...(applied.decisionChannel ? { decision_channel: applied.decisionChannel } : {}) };
|
|
@@ -19,9 +19,11 @@ export declare function declaredAppMimeTypes(declared: unknown): string[];
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function approvalAppEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
21
21
|
export declare function approvalAppClientAllowed(clientName: unknown, env?: NodeJS.ProcessEnv): boolean;
|
|
22
|
-
export declare function rememberApprovalCapabilities(session: object, declared: unknown, clientName?: unknown): ApprovalClientCapabilities;
|
|
22
|
+
export declare function rememberApprovalCapabilities(session: object, declared: unknown, clientName?: unknown, clientVersion?: unknown): ApprovalClientCapabilities;
|
|
23
23
|
/** Name from this in-process session's initialize clientInfo; undefined when none was declared. */
|
|
24
24
|
export declare function approvalClientName(session: object): string | undefined;
|
|
25
|
+
export declare function approvalClientVersion(session: object): string | undefined;
|
|
26
|
+
export declare function approvalDeclaredCapabilities(session: object): unknown;
|
|
25
27
|
/**
|
|
26
28
|
* Declared-capability probe for this in-process session. `appsAllowed: false` removes the Apps
|
|
27
29
|
* tier from the choice (flag off / client not allowlisted), so a lower tier can still be used.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const MCP_APP_EXTENSION = 'io.modelcontextprotocol/ui';
|
|
2
2
|
export const MCP_APP_MIME_TYPE = 'text/html;profile=mcp-app';
|
|
3
3
|
const sessions = new WeakMap();
|
|
4
|
+
const clientVersions = new WeakMap();
|
|
4
5
|
const clientNames = new WeakMap();
|
|
5
6
|
const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
6
7
|
/** Reads only declared initialize capabilities. No user agent or client-name guessing. */
|
|
@@ -40,19 +41,33 @@ export function approvalAppClientAllowed(clientName, env = process.env) {
|
|
|
40
41
|
const allowed = (env.MCP_APPROVAL_APP_CLIENTS ?? '').split(',').map(name => name.trim()).filter(Boolean);
|
|
41
42
|
return allowed.includes(clientName.trim());
|
|
42
43
|
}
|
|
43
|
-
export function rememberApprovalCapabilities(session, declared, clientName) {
|
|
44
|
+
export function rememberApprovalCapabilities(session, declared, clientName, clientVersion) {
|
|
44
45
|
const capabilities = probeApprovalCapabilities(declared);
|
|
45
46
|
sessions.set(session, capabilities);
|
|
46
47
|
if (typeof clientName === 'string')
|
|
47
48
|
clientNames.set(session, clientName);
|
|
48
49
|
else
|
|
49
50
|
clientNames.delete(session);
|
|
51
|
+
if (typeof clientVersion === 'string')
|
|
52
|
+
clientVersions.set(session, clientVersion);
|
|
53
|
+
else
|
|
54
|
+
clientVersions.delete(session);
|
|
50
55
|
return capabilities;
|
|
51
56
|
}
|
|
52
57
|
/** Name from this in-process session's initialize clientInfo; undefined when none was declared. */
|
|
53
58
|
export function approvalClientName(session) {
|
|
54
59
|
return clientNames.get(session);
|
|
55
60
|
}
|
|
61
|
+
export function approvalClientVersion(session) {
|
|
62
|
+
return clientVersions.get(session);
|
|
63
|
+
}
|
|
64
|
+
export function approvalDeclaredCapabilities(session) {
|
|
65
|
+
const caps = sessions.get(session);
|
|
66
|
+
return { ...(caps?.elicitation_form || caps?.elicitation_url ? { elicitation: {
|
|
67
|
+
...(caps.elicitation_form ? { form: {} } : {}), ...(caps.elicitation_url ? { url: {} } : {}),
|
|
68
|
+
} } : {}), extensions: caps?.mcp_app
|
|
69
|
+
? { [MCP_APP_EXTENSION]: { mimeTypes: [MCP_APP_MIME_TYPE] } } : {} };
|
|
70
|
+
}
|
|
56
71
|
/**
|
|
57
72
|
* Declared-capability probe for this in-process session. `appsAllowed: false` removes the Apps
|
|
58
73
|
* tier from the choice (flag off / client not allowlisted), so a lower tier can still be used.
|
|
@@ -72,7 +87,7 @@ export function installApprovalCapabilityProbe(session) {
|
|
|
72
87
|
const previous = session.oninitialized;
|
|
73
88
|
session.oninitialized = () => {
|
|
74
89
|
const info = object(session.getClientVersion?.());
|
|
75
|
-
rememberApprovalCapabilities(session, session.getClientCapabilities(), info?.name);
|
|
90
|
+
rememberApprovalCapabilities(session, session.getClientCapabilities(), info?.name, info?.version);
|
|
76
91
|
previous?.call(session);
|
|
77
92
|
};
|
|
78
93
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare const ELICITATION_AUTO_ACCEPT_SUSPECT_MS = 2000;
|
|
2
|
+
export type ElicitationEvidence = ReturnType<typeof elicitationEvidence>;
|
|
3
|
+
export type DecisionEvidence = {
|
|
4
|
+
answer_latency_ms: number;
|
|
5
|
+
auto_accept_suspected: boolean;
|
|
6
|
+
channel: 'mcp_elicitation';
|
|
7
|
+
};
|
|
8
|
+
export declare function elicitationEvidence(input: {
|
|
9
|
+
promptShownAt: string;
|
|
10
|
+
answeredAt: string;
|
|
11
|
+
action: 'accept' | 'decline';
|
|
12
|
+
content?: unknown;
|
|
13
|
+
requestId?: string | number;
|
|
14
|
+
clientName?: unknown;
|
|
15
|
+
clientVersion?: unknown;
|
|
16
|
+
capabilities?: unknown;
|
|
17
|
+
userAgent?: unknown;
|
|
18
|
+
}): {
|
|
19
|
+
prompt_shown_at: string;
|
|
20
|
+
answered_at: string;
|
|
21
|
+
answer_latency_ms: number;
|
|
22
|
+
auto_accept_suspected: boolean;
|
|
23
|
+
client_name?: string | undefined;
|
|
24
|
+
client_version?: string | undefined;
|
|
25
|
+
client_capabilities: {
|
|
26
|
+
elicitation_form: boolean;
|
|
27
|
+
elicitation_url: boolean;
|
|
28
|
+
apps: boolean;
|
|
29
|
+
};
|
|
30
|
+
response_action: "accept" | "decline";
|
|
31
|
+
response_content_keys: string[];
|
|
32
|
+
user_agent?: string | undefined;
|
|
33
|
+
jsonrpc_request_id_hash?: string | undefined;
|
|
34
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { probeApprovalCapabilities } from './approval-capabilities.js';
|
|
3
|
+
export const ELICITATION_AUTO_ACCEPT_SUSPECT_MS = 2000;
|
|
4
|
+
// Metadata is client-declared, never proof of a human. Reject obvious private text.
|
|
5
|
+
const metadata = (value, max) => typeof value === 'string' && !/[@\r\n]|bearer\s|\b(?:sk_|snk_|eyJ)/i.test(value)
|
|
6
|
+
? value.slice(0, max) : undefined;
|
|
7
|
+
export function elicitationEvidence(input) {
|
|
8
|
+
const latency = Math.max(0, Math.trunc(Date.parse(input.answeredAt) - Date.parse(input.promptShownAt)));
|
|
9
|
+
const capabilities = probeApprovalCapabilities(input.capabilities);
|
|
10
|
+
const name = metadata(input.clientName, 200), version = metadata(input.clientVersion, 100);
|
|
11
|
+
const userAgent = metadata(input.userAgent, 120);
|
|
12
|
+
return {
|
|
13
|
+
prompt_shown_at: input.promptShownAt, answered_at: input.answeredAt,
|
|
14
|
+
answer_latency_ms: latency,
|
|
15
|
+
auto_accept_suspected: input.action === 'accept' && latency < ELICITATION_AUTO_ACCEPT_SUSPECT_MS,
|
|
16
|
+
...(name ? { client_name: name } : {}), ...(version ? { client_version: version } : {}),
|
|
17
|
+
client_capabilities: { elicitation_form: capabilities.elicitation_form,
|
|
18
|
+
elicitation_url: capabilities.elicitation_url, apps: capabilities.mcp_app },
|
|
19
|
+
response_action: input.action,
|
|
20
|
+
response_content_keys: input.content && typeof input.content === 'object' && !Array.isArray(input.content)
|
|
21
|
+
? Object.keys(input.content).filter(key => /^[A-Za-z_][A-Za-z0-9_]{0,119}$/.test(key)) : [],
|
|
22
|
+
...(userAgent ? { user_agent: userAgent } : {}),
|
|
23
|
+
...(input.requestId !== undefined ? {
|
|
24
|
+
jsonrpc_request_id_hash: createHash('sha256').update(String(input.requestId)).digest('hex').slice(0, 8),
|
|
25
|
+
} : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -5,7 +5,9 @@ export declare function isElicitationResponse(body: unknown): boolean;
|
|
|
5
5
|
/** Auth has already run. Unknown and malformed responses are deliberately indistinguishable.
|
|
6
6
|
* No decision can enter this path via a tools/call, model text, or nested tool arguments.
|
|
7
7
|
*/
|
|
8
|
-
export declare function recordElicitationResponse(body: unknown, identity: HumanDecisionIdentity
|
|
8
|
+
export declare function recordElicitationResponse(body: unknown, identity: HumanDecisionIdentity, metadata?: {
|
|
9
|
+
userAgent?: string | null;
|
|
10
|
+
}): Promise<void>;
|
|
9
11
|
/** Insert before sending, using the exact wire id (a UUID, since each stateless SDK starts
|
|
10
12
|
* its request counter at zero). Poll the persisted prompt; never await a response on the old
|
|
11
13
|
* SDK Server, because the client's answer necessarily arrives at a fresh Server via POST.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { elicitationEvidence } from './approval-elicitation-evidence.js';
|
|
1
2
|
import { ELICITATION_HOLD_MS } from './approval-elicitation.js';
|
|
2
3
|
import { randomUUID } from 'node:crypto';
|
|
3
4
|
import { approvalStore } from './tester-requests-store.js';
|
|
@@ -16,7 +17,8 @@ export function isElicitationResponse(body) {
|
|
|
16
17
|
/** Auth has already run. Unknown and malformed responses are deliberately indistinguishable.
|
|
17
18
|
* No decision can enter this path via a tools/call, model text, or nested tool arguments.
|
|
18
19
|
*/
|
|
19
|
-
export async function recordElicitationResponse(body, identity) {
|
|
20
|
+
export async function recordElicitationResponse(body, identity, metadata = {}) {
|
|
21
|
+
const answeredAt = new Date().toISOString();
|
|
20
22
|
const message = object(body);
|
|
21
23
|
if (!isElicitationResponse(message) || message?.jsonrpc !== '2.0' ||
|
|
22
24
|
(typeof message.id !== 'string' && typeof message.id !== 'number') ||
|
|
@@ -55,6 +57,36 @@ export async function recordElicitationResponse(body, identity) {
|
|
|
55
57
|
});
|
|
56
58
|
if (rpcError || data !== true)
|
|
57
59
|
return;
|
|
60
|
+
// Evidence is additive. Neither metadata lookup nor audit failure can undo the decision.
|
|
61
|
+
try {
|
|
62
|
+
const { data: declared, error: declarationError } = await client.from('mcp_client_capabilities')
|
|
63
|
+
.select('client_name, client_version, elicitation_form, elicitation_url, mcp_app')
|
|
64
|
+
.eq('oauth_token_id', identity.oauthTokenId).gt('expires_at', answeredAt).maybeSingle();
|
|
65
|
+
const info = declarationError ? null : declared;
|
|
66
|
+
const capabilities = { elicitation: {
|
|
67
|
+
...(info?.elicitation_form === true ? { form: {} } : {}),
|
|
68
|
+
...(info?.elicitation_url === true ? { url: {} } : {}),
|
|
69
|
+
}, extensions: info?.mcp_app === true
|
|
70
|
+
? { 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html;profile=mcp-app'] } } : {} };
|
|
71
|
+
// Absent support must not become the legacy elicitation:{} form declaration.
|
|
72
|
+
if (!info?.elicitation_form && !info?.elicitation_url)
|
|
73
|
+
delete capabilities.elicitation;
|
|
74
|
+
const { error: auditError } = await client.from('audit_log').insert({
|
|
75
|
+
project_id: prompt.project_id, actor_type: 'user', actor_id: identity.actorId,
|
|
76
|
+
action: 'approval.elicitation_answered', resource_type: 'approval', resource_id: prompt.approval_id,
|
|
77
|
+
details: { channel: 'mcp_elicitation', ...elicitationEvidence({
|
|
78
|
+
promptShownAt: new Date(prompt.created_at).toISOString(), answeredAt,
|
|
79
|
+
action: result.action, content: result.content, requestId: message.id,
|
|
80
|
+
clientName: info?.client_name, clientVersion: info?.client_version,
|
|
81
|
+
capabilities, userAgent: metadata.userAgent,
|
|
82
|
+
}) },
|
|
83
|
+
});
|
|
84
|
+
if (auditError)
|
|
85
|
+
console.warn('[approval_evidence]', { reason: 'audit_write_failed' });
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
console.warn('[approval_evidence]', { reason: 'audit_write_failed' });
|
|
89
|
+
}
|
|
58
90
|
}
|
|
59
91
|
catch {
|
|
60
92
|
// Never reveal whether a prompt exists, and never fall back to non-atomic REST writes.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ElicitationEvidence } from './approval-elicitation-evidence.js';
|
|
1
2
|
import { type HumanDecisionIdentity } from './approval-human-eligibility.js';
|
|
2
3
|
import type { ElicitRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
3
4
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
@@ -9,6 +10,7 @@ type HumanCallback = {
|
|
|
9
10
|
interactionId: string;
|
|
10
11
|
connectionId: string;
|
|
11
12
|
decision: Decision;
|
|
13
|
+
evidence?: ElicitationEvidence;
|
|
12
14
|
}): Promise<boolean>;
|
|
13
15
|
};
|
|
14
16
|
/** Injected at authenticated transport startup, never taken from tool arguments. */
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { elicitationEvidence } from './approval-elicitation-evidence.js';
|
|
1
2
|
import { isHumanRequestableAdminTool } from './admin-tools.js';
|
|
2
3
|
import { humanApprovalEligible } from './approval-human-eligibility.js';
|
|
3
4
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
4
5
|
import { randomUUID } from 'node:crypto';
|
|
5
6
|
import { approvalDisplayFact, buildApprovalPreview, isEmailApprovalAction, isHumanApprovalChannel } from '@sendinel/domain';
|
|
6
7
|
import { approvalStore } from './tester-requests-store.js';
|
|
7
|
-
import { approvalAppClientAllowed, approvalClientName, approval_channel_available, probeApprovalCapabilities } from './approval-capabilities.js';
|
|
8
|
+
import { approvalAppClientAllowed, approvalClientName, approvalClientVersion, approvalDeclaredCapabilities, approval_channel_available, probeApprovalCapabilities } from './approval-capabilities.js';
|
|
8
9
|
// Keyed by the SDK Server, not the McpServer wrapper: other installers (the Apps tier) wrap
|
|
9
10
|
// McpServer in a Proxy, which changes the wrapper's identity but never the connection's Server.
|
|
10
11
|
const sessions = new WeakMap();
|
|
@@ -100,7 +101,9 @@ export async function elicitApproval(result, frozen, projectId) {
|
|
|
100
101
|
}
|
|
101
102
|
const callback = context.createHumanDecision({ approvalId: result.approval_id, projectId,
|
|
102
103
|
connectionId: context.connectionId, userId: context.userId, channel: 'mcp_elicitation' });
|
|
104
|
+
const promptShownAt = new Date().toISOString();
|
|
103
105
|
const response = await Promise.race([context.server.server.elicitInput(params, options), stopped]);
|
|
106
|
+
const answeredAt = new Date().toISOString();
|
|
104
107
|
if (controller.signal.aborted)
|
|
105
108
|
return exit(endReason ?? 'request_aborted');
|
|
106
109
|
// Only the client's action is a decision. Extra content is untrusted and ignored.
|
|
@@ -108,7 +111,12 @@ export async function elicitApproval(result, frozen, projectId) {
|
|
|
108
111
|
return exit('client_cancelled');
|
|
109
112
|
const decision = response.action === 'accept' ? 'approve' : 'reject';
|
|
110
113
|
const recorded = await Promise.race([callback.complete({ interactionId: callback.interactionId,
|
|
111
|
-
connectionId: context.connectionId, decision
|
|
114
|
+
connectionId: context.connectionId, decision, evidence: elicitationEvidence({
|
|
115
|
+
promptShownAt, answeredAt, action: response.action, content: response.content,
|
|
116
|
+
requestId: context.requestId, clientName: approvalClientName(context.server.server),
|
|
117
|
+
clientVersion: approvalClientVersion(context.server.server),
|
|
118
|
+
capabilities: approvalDeclaredCapabilities(context.server.server),
|
|
119
|
+
}) }), stopped]);
|
|
112
120
|
return recorded ? { ...result, decision_status: decision === 'approve' ? 'approved' : 'rejected' } : exit('decision_refused');
|
|
113
121
|
}
|
|
114
122
|
const elicitationId = randomUUID();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ElicitationEvidence } from './approval-elicitation-evidence.js';
|
|
1
2
|
/**
|
|
2
3
|
* Server integration only. The transport creates this closure for one authenticated human
|
|
3
4
|
* interaction and retains it outside the tool registry. Neither its id nor decision is a tool
|
|
@@ -15,5 +16,6 @@ export declare function createHumanApprovalCallback(binding: {
|
|
|
15
16
|
interactionId: string;
|
|
16
17
|
connectionId: string;
|
|
17
18
|
decision: 'approve' | 'reject';
|
|
19
|
+
evidence?: ElicitationEvidence;
|
|
18
20
|
}): Promise<boolean>;
|
|
19
21
|
};
|
|
@@ -38,7 +38,7 @@ export function createHumanApprovalCallback(binding) {
|
|
|
38
38
|
const { error: auditError } = await client.from('audit_log').insert({
|
|
39
39
|
project_id: binding.projectId, actor_type: 'user', actor_id: binding.userId,
|
|
40
40
|
action: 'approval.submitted', resource_type: 'approval', resource_id: binding.approvalId,
|
|
41
|
-
details: { status: response.decision === 'approve' ? 'approved' : 'rejected', channel: binding.channel,
|
|
41
|
+
details: { ...(binding.channel === 'mcp_elicitation' ? response.evidence : {}), status: response.decision === 'approve' ? 'approved' : 'rejected', channel: binding.channel,
|
|
42
42
|
connection_id: binding.connectionId, interaction_id: interactionId, payload_hash: data.payload_hash },
|
|
43
43
|
});
|
|
44
44
|
if (auditError)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.160",
|
|
4
4
|
"description": "AI-controlled email operations control plane for campaigns, contacts, segments, analytics, and multi-provider sending via MCP.",
|
|
5
5
|
"mcpName": "ai.sendinel/mcp-server",
|
|
6
6
|
"type": "module",
|