@sendinel/mcp-server 1.0.124 → 1.0.126
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 +2 -2
- package/build/db.d.ts +2 -0
- package/build/db.js +1 -0
- package/build/lib/action-approvals.js +13 -2
- package/build/lib/approval-chat-confirm.d.ts +24 -0
- package/build/lib/approval-chat-confirm.js +90 -0
- package/build/lib/approval-human-eligibility.d.ts +23 -1
- package/build/lib/approval-human-eligibility.js +19 -2
- package/build/server.d.ts +2 -0
- package/build/server.js +1 -1
- package/build/tools/approvals.d.ts +2 -1
- package/build/tools/approvals.js +14 -1
- package/build/tools/gateway.d.ts +1 -0
- package/build/tools/gateway.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ preserves REST outcomes, and redacts failures. See the
|
|
|
18
18
|
[event-triggered campaign guide](../../docs/event-triggered-campaigns.md) for
|
|
19
19
|
inputs, idempotency, subscription requirements, and conditional step examples.
|
|
20
20
|
|
|
21
|
-
Sendinel currently exposes 35 tool groups and
|
|
21
|
+
Sendinel currently exposes 35 tool groups and 342 tools (gateway mode exposes 4 meta-tools, sharing how_to with full mode). These counts are checked against the tool registry in CI, so this table matches what ships.
|
|
22
22
|
|
|
23
23
|
| Group | Count | Purpose |
|
|
24
24
|
|-------|-------|---------|
|
|
@@ -27,7 +27,7 @@ Sendinel currently exposes 35 tool groups and 341 tools (gateway mode exposes 4
|
|
|
27
27
|
| `advisor` | 6 | Campaign advisor: recommendations, benchmarks, subject/content/send-time guidance |
|
|
28
28
|
| `ai-work` | 9 | AI work items, recommended segments, context events, agent proposals |
|
|
29
29
|
| `analytics` | 15 | Stats, portfolio analytics, project switching, domain health, engagement, deliverability, revenue analytics |
|
|
30
|
-
| `approvals` |
|
|
30
|
+
| `approvals` | 3 | Poll and list risk-tiered action approvals |
|
|
31
31
|
| `automations` | 7 | Automation triggers, previews, and response-gated escalation ladders |
|
|
32
32
|
| `brand_kit` | 6 | Brand Core text import, media library upload/URL ingest, asset-context reads, project logo selection |
|
|
33
33
|
| `campaigns` | 44 | Full campaign lifecycle: create, edit, approve, launch, enroll, schedule, sequence templates, convert imported campaigns |
|
package/build/db.d.ts
CHANGED
|
@@ -40,6 +40,7 @@ export declare function runWithClient<T>(client: any, fn: () => Promise<T>, requ
|
|
|
40
40
|
actorType?: string;
|
|
41
41
|
actorId?: string | null;
|
|
42
42
|
oauthTokenId?: string | null;
|
|
43
|
+
chatConfirmEnabled?: boolean;
|
|
43
44
|
invitationProjectIds?: string[] | null;
|
|
44
45
|
authKind?: string | null;
|
|
45
46
|
oauthClientId?: string | null;
|
|
@@ -66,6 +67,7 @@ export declare function requestContext(): {
|
|
|
66
67
|
actorType?: string;
|
|
67
68
|
actorId?: string | null;
|
|
68
69
|
oauthTokenId?: string | null;
|
|
70
|
+
chatConfirmEnabled?: boolean;
|
|
69
71
|
invitationProjectIds?: string[] | null;
|
|
70
72
|
authKind?: string | null;
|
|
71
73
|
oauthClientId?: string | null;
|
package/build/db.js
CHANGED
|
@@ -78,6 +78,7 @@ export function requestContext() {
|
|
|
78
78
|
actorType: ctx.actorType,
|
|
79
79
|
actorId: ctx.actorId,
|
|
80
80
|
oauthTokenId: ctx.oauthTokenId,
|
|
81
|
+
chatConfirmEnabled: ctx.chatConfirmEnabled,
|
|
81
82
|
invitationProjectIds: ctx.invitationProjectIds,
|
|
82
83
|
authKind: ctx.authKind,
|
|
83
84
|
oauthClientId: ctx.oauthClientId,
|
|
@@ -311,6 +311,9 @@ export async function requestApproval(action, params, apiKeyId, channelContext,
|
|
|
311
311
|
catch { /* Missing display data omits the project line. */ }
|
|
312
312
|
const approveUrl = approvalPageUrl(data.id);
|
|
313
313
|
const previewText = buildApprovalPreview({ action, params: frozenParams, summary, projectName }).plain_summary;
|
|
314
|
+
const chatGuidance = tier === 'gated' && await humanApprovalEligible(action, getProjectId(), actor, true)
|
|
315
|
+
? `\nAsk the user in plain chat: "${previewText.split('\n')[0].trim()}". Only after they say yes, call confirm_approval with approval_id, action, echo (the first line of preview_text) and user_reply (their exact words), then call the original tool again with approval_id.`
|
|
316
|
+
: '';
|
|
314
317
|
const result = await elicitApproval({
|
|
315
318
|
requires_approval: true,
|
|
316
319
|
...(await offerApprovalApp({ id: data.id, action, params: frozenParams, summary, tier, expires_at: expiresAt,
|
|
@@ -323,7 +326,7 @@ export async function requestApproval(action, params, apiKeyId, channelContext,
|
|
|
323
326
|
approve_via: 'Open approve_url (works on a phone) or Dashboard → Settings → Pending Approvals',
|
|
324
327
|
approve_url: approveUrl,
|
|
325
328
|
preview_text: previewText,
|
|
326
|
-
message: `Tap to approve: ${approveUrl}\n${previewText}\nIt stays open for 24 hours. In a terminal with prompts you can also answer the on-screen Accept/Decline
|
|
329
|
+
message: `Tap to approve: ${approveUrl}\n${previewText}\nIt stays open for 24 hours. In a terminal with prompts you can also answer the on-screen Accept/Decline.${chatGuidance}`,
|
|
327
330
|
}, { action, params: frozenParams, summary, projectName }, getProjectId());
|
|
328
331
|
if (result.decision_status === 'approved') {
|
|
329
332
|
return { ...result, message: 'Approved. Call this tool again with the same arguments plus approval_id to run it.' };
|
|
@@ -382,9 +385,15 @@ export async function checkApproval(approvalId, options = {}) {
|
|
|
382
385
|
}
|
|
383
386
|
if (data.used_at || data.status === 'used')
|
|
384
387
|
return { approved: false, status: 'consumed' };
|
|
388
|
+
// Chat decisions stay bound to the same live human grant at consumption, for every action.
|
|
389
|
+
if (data.status === 'approved' && data.decided_via === 'chat_confirm' &&
|
|
390
|
+
(requestContext()?.oauthTokenId !== data.oauth_token_id || requestContext()?.actorId !== data.decided_by ||
|
|
391
|
+
!await humanApprovalEligible(data.action, getProjectId(), requestContext(), true))) {
|
|
392
|
+
return { approved: false, status: 'human_decision_required' };
|
|
393
|
+
}
|
|
385
394
|
if (data.status === 'approved' && (!isHumanApprovalChannel(data.decided_via) ||
|
|
386
395
|
(isHumanRequestableAdminTool(data.action) && data.decided_via !== 'dashboard' &&
|
|
387
|
-
(!['mcp_app', 'mcp_elicitation'].includes(data.decided_via) ||
|
|
396
|
+
(!['mcp_app', 'mcp_elicitation', 'chat_confirm'].includes(data.decided_via) ||
|
|
388
397
|
!await humanApprovalEligible(data.action, getProjectId(), { projectId: getProjectId(),
|
|
389
398
|
actorId: data.decided_by, oauthTokenId: data.oauth_token_id, authKind: 'oauth', channelVerified: true }))))) {
|
|
390
399
|
return { approved: false, status: 'human_decision_required' };
|
|
@@ -423,6 +432,8 @@ export async function checkApproval(approvalId, options = {}) {
|
|
|
423
432
|
.eq('decided_via', data.decided_via)
|
|
424
433
|
.eq('params', JSON.stringify(data.params))
|
|
425
434
|
.gt('requested_at', new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString());
|
|
435
|
+
if (data.decided_via === 'chat_confirm')
|
|
436
|
+
claimQuery.eq('oauth_token_id', requestContext().oauthTokenId).eq('decided_by', requestContext().actorId);
|
|
426
437
|
const boundClaim = data.payload_hash ? claimQuery.eq('payload_hash', data.payload_hash) : claimQuery.is('payload_hash', null);
|
|
427
438
|
const { data: claimed, error: claimError } = await boundClaim.select('decided_by').maybeSingle();
|
|
428
439
|
if (claimError || !claimed) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const confirmApprovalInput: {
|
|
3
|
+
approval_id: z.ZodString;
|
|
4
|
+
action: z.ZodString;
|
|
5
|
+
echo: z.ZodString;
|
|
6
|
+
user_reply: z.ZodString;
|
|
7
|
+
};
|
|
8
|
+
declare const inputSchema: z.ZodObject<{
|
|
9
|
+
approval_id: z.ZodString;
|
|
10
|
+
action: z.ZodString;
|
|
11
|
+
echo: z.ZodString;
|
|
12
|
+
user_reply: z.ZodString;
|
|
13
|
+
}, z.core.$strip>;
|
|
14
|
+
export declare const CHAT_CONFIRM_NOT_ENABLED = "Chat confirm is not enabled for this connection. Use the on-screen prompt or approve_url.";
|
|
15
|
+
/** This opt-in boundary is protected by the Claude Code PreToolUse hook, not elicitation. */
|
|
16
|
+
export declare function confirmApproval(input: z.infer<typeof inputSchema>): Promise<{
|
|
17
|
+
code: string;
|
|
18
|
+
error: string;
|
|
19
|
+
} | {
|
|
20
|
+
decision_status: 'approved';
|
|
21
|
+
approval_id: string;
|
|
22
|
+
next: string;
|
|
23
|
+
}>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { buildApprovalPreview } from '@sendinel/domain';
|
|
3
|
+
import { requestContext } from '../db.js';
|
|
4
|
+
import { getProjectId } from '../project.js';
|
|
5
|
+
import { humanApprovalEligible, chatConfirmConnectionEnabled } from './approval-human-eligibility.js';
|
|
6
|
+
import { approvalStore } from './tester-requests-store.js';
|
|
7
|
+
import { approvalPayloadHash } from './action-approvals.js';
|
|
8
|
+
export const confirmApprovalInput = {
|
|
9
|
+
approval_id: z.string().uuid(),
|
|
10
|
+
action: z.string(),
|
|
11
|
+
echo: z.string().max(300),
|
|
12
|
+
user_reply: z.string().min(1).max(500),
|
|
13
|
+
};
|
|
14
|
+
const inputSchema = z.object(confirmApprovalInput);
|
|
15
|
+
const refuse = (code, error, extra = {}) => ({ code, error, ...extra });
|
|
16
|
+
export const CHAT_CONFIRM_NOT_ENABLED = 'Chat confirm is not enabled for this connection. Use the on-screen prompt or approve_url.';
|
|
17
|
+
/** This opt-in boundary is protected by the Claude Code PreToolUse hook, not elicitation. */
|
|
18
|
+
export async function confirmApproval(input) {
|
|
19
|
+
const actor = requestContext();
|
|
20
|
+
if (actor?.chatConfirmEnabled !== true)
|
|
21
|
+
return refuse('chat_confirm_not_enabled', CHAT_CONFIRM_NOT_ENABLED);
|
|
22
|
+
if (!inputSchema.safeParse(input).success)
|
|
23
|
+
return refuse('chat_confirm_invalid_input', 'Invalid confirm_approval arguments. user_reply must contain 1 to 500 characters and echo at most 300.');
|
|
24
|
+
const projectId = getProjectId();
|
|
25
|
+
if (!await humanApprovalEligible(input.action, projectId, actor)) {
|
|
26
|
+
return refuse('chat_confirm_ineligible', 'Chat confirm requires an enabled human OAuth connection and an owner/admin for admin actions.');
|
|
27
|
+
}
|
|
28
|
+
if (!await chatConfirmConnectionEnabled(actor))
|
|
29
|
+
return refuse('chat_confirm_not_enabled', CHAT_CONFIRM_NOT_ENABLED);
|
|
30
|
+
try {
|
|
31
|
+
const source = approvalStore();
|
|
32
|
+
const client = typeof source.schema === 'function' ? source.schema('email') : source;
|
|
33
|
+
const { data: approval, error: loadError } = await client.from('action_approvals')
|
|
34
|
+
.select('id, project_id, oauth_token_id, action, tier, status, used_at, requested_at, expires_at, params, summary, payload_hash')
|
|
35
|
+
.eq('id', input.approval_id).eq('project_id', projectId).eq('oauth_token_id', actor.oauthTokenId).maybeSingle();
|
|
36
|
+
if (loadError)
|
|
37
|
+
return refuse('chat_confirm_lookup_failed', 'Unable to verify the approval. Nothing was recorded.');
|
|
38
|
+
if (!approval || approval.project_id !== projectId || approval.oauth_token_id !== actor.oauthTokenId) {
|
|
39
|
+
return refuse('chat_confirm_not_found', 'Approval not found for this connection and project.');
|
|
40
|
+
}
|
|
41
|
+
if (approval.tier !== 'gated')
|
|
42
|
+
return refuse('chat_confirm_tier', 'Chat confirm supports gated approvals only. Protected actions require dashboard approval.');
|
|
43
|
+
if (approval.status !== 'pending' || approval.used_at != null)
|
|
44
|
+
return refuse('chat_confirm_not_pending', 'Approval is already decided or used. Request a new approval.');
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
const requested = Date.parse(approval.requested_at);
|
|
47
|
+
if (!(Date.parse(approval.expires_at) > now))
|
|
48
|
+
return refuse('chat_confirm_expired', 'Approval has expired. Request a new approval, or tap the approve_url.');
|
|
49
|
+
if (!Number.isFinite(requested) || requested > now || requested < now - 600_000) {
|
|
50
|
+
return refuse('chat_confirm_too_old', 'Chat confirm requires an approval requested within the last 10 minutes. Request a new approval, or tap the approve_url.');
|
|
51
|
+
}
|
|
52
|
+
if (input.action !== approval.action)
|
|
53
|
+
return refuse('chat_confirm_action_mismatch', 'Action does not match the approval.');
|
|
54
|
+
if (!approval.params || approvalPayloadHash(approval.action, approval.params) !== approval.payload_hash) {
|
|
55
|
+
return refuse('chat_confirm_payload_changed', 'Approval payload could not be verified. Request a new approval.');
|
|
56
|
+
}
|
|
57
|
+
const { data: project, error: projectError } = await client.from('projects').select('name').eq('id', projectId).maybeSingle();
|
|
58
|
+
if (projectError || !project || typeof project.name !== 'string')
|
|
59
|
+
return refuse('chat_confirm_lookup_failed', 'Unable to verify the approval. Nothing was recorded.');
|
|
60
|
+
const echo = buildApprovalPreview({ action: approval.action, params: approval.params, summary: approval.summary,
|
|
61
|
+
projectName: project.name }).plain_summary.split('\n')[0].trim();
|
|
62
|
+
if (!echo || input.echo.trim() !== echo)
|
|
63
|
+
return refuse('chat_confirm_echo_mismatch', `Echo does not match the approval. Use this first line: ${echo}`, { echo });
|
|
64
|
+
// Re-check time and frozen bindings at the write, so replay/concurrent changes cannot win.
|
|
65
|
+
const decidedAt = new Date().toISOString();
|
|
66
|
+
const { data: decided, error: decisionError } = await client.from('action_approvals')
|
|
67
|
+
.update({ status: 'approved', decided_via: 'chat_confirm', decided_by: actor.actorId, decided_at: decidedAt })
|
|
68
|
+
.eq('id', input.approval_id).eq('project_id', projectId).eq('oauth_token_id', actor.oauthTokenId)
|
|
69
|
+
.eq('action', input.action).eq('tier', 'gated').eq('status', 'pending').is('used_at', null)
|
|
70
|
+
.gt('expires_at', decidedAt).eq('requested_at', approval.requested_at)
|
|
71
|
+
.gt('requested_at', new Date(Date.now() - 600_000 - 1).toISOString())
|
|
72
|
+
.eq('payload_hash', approval.payload_hash).eq('params', JSON.stringify(approval.params))
|
|
73
|
+
.select('id, payload_hash').maybeSingle();
|
|
74
|
+
if (decisionError || !decided)
|
|
75
|
+
return refuse('chat_confirm_not_pending', 'Approval is already decided, used or no longer valid. Request a new approval.');
|
|
76
|
+
const { error: auditError } = await client.from('audit_log').insert({
|
|
77
|
+
project_id: projectId, actor_type: 'user', actor_id: actor.actorId,
|
|
78
|
+
action: 'approval.submitted', resource_type: 'approval', resource_id: input.approval_id,
|
|
79
|
+
details: { status: 'approved', channel: 'chat_confirm', user_reply: input.user_reply.replace(/[\p{Cc}]/gu, '').slice(0, 500),
|
|
80
|
+
echo, payload_hash: decided.payload_hash, connection_id: `oauth:${actor.oauthTokenId}` },
|
|
81
|
+
});
|
|
82
|
+
if (auditError)
|
|
83
|
+
return refuse('chat_confirm_audit_failed', 'Approval recorded, but decision audit failed. Check approval status before retrying.');
|
|
84
|
+
return { decision_status: 'approved', approval_id: input.approval_id,
|
|
85
|
+
next: `Call ${input.action} again with the same arguments plus approval_id.` };
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return refuse('chat_confirm_failed', 'Chat confirm failed. Check approval status before retrying.');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -4,10 +4,32 @@ export type HumanDecisionIdentity = {
|
|
|
4
4
|
oauthTokenId?: string | null;
|
|
5
5
|
authKind?: string | null;
|
|
6
6
|
channelVerified?: boolean;
|
|
7
|
+
chatConfirmEnabled?: boolean;
|
|
7
8
|
scopes?: readonly string[];
|
|
8
9
|
};
|
|
9
10
|
/** Shared transport boundary, never populated from tool arguments. Re-read the grant and
|
|
10
11
|
* (for admin requests) the project's org membership on every prompt and every decision.
|
|
11
12
|
* Missing connection_role is NOT a human grant: it must be exactly SQL NULL.
|
|
12
13
|
*/
|
|
13
|
-
export declare function humanApprovalEligible(action: string, projectId: string, identity?: HumanDecisionIdentity | undefined): Promise<boolean>;
|
|
14
|
+
export declare function humanApprovalEligible(action: string, projectId: string, identity?: HumanDecisionIdentity | undefined, requireChatConfirm?: boolean): Promise<boolean>;
|
|
15
|
+
/** Opt-in is a live grant fact as well as a transport snapshot; errors never enable it. */
|
|
16
|
+
export declare function chatConfirmConnectionEnabled(identity?: {
|
|
17
|
+
scopes?: readonly string[];
|
|
18
|
+
approvedTesterAction?: string;
|
|
19
|
+
projectId?: string;
|
|
20
|
+
approvalMode?: string;
|
|
21
|
+
channel?: string;
|
|
22
|
+
channelVerified?: boolean;
|
|
23
|
+
actorType?: string;
|
|
24
|
+
actorId?: string | null;
|
|
25
|
+
oauthTokenId?: string | null;
|
|
26
|
+
chatConfirmEnabled?: boolean;
|
|
27
|
+
invitationProjectIds?: string[] | null;
|
|
28
|
+
authKind?: string | null;
|
|
29
|
+
oauthClientId?: string | null;
|
|
30
|
+
agentLabel?: string | null;
|
|
31
|
+
keyId?: string | null;
|
|
32
|
+
workspaceId?: string | null;
|
|
33
|
+
kid?: string | null;
|
|
34
|
+
v1Caller?: import("../db.js").V1Caller;
|
|
35
|
+
} | undefined): Promise<boolean>;
|
|
@@ -5,8 +5,9 @@ import { isHumanRequestableAdminTool } from './admin-tools.js';
|
|
|
5
5
|
* (for admin requests) the project's org membership on every prompt and every decision.
|
|
6
6
|
* Missing connection_role is NOT a human grant: it must be exactly SQL NULL.
|
|
7
7
|
*/
|
|
8
|
-
export async function humanApprovalEligible(action, projectId, identity = requestContext()) {
|
|
8
|
+
export async function humanApprovalEligible(action, projectId, identity = requestContext(), requireChatConfirm = false) {
|
|
9
9
|
if (!identity || identity.channelVerified !== true || identity.authKind !== 'oauth' ||
|
|
10
|
+
(requireChatConfirm && identity.chatConfirmEnabled !== true) ||
|
|
10
11
|
!identity.actorId || !identity.oauthTokenId || identity.projectId !== projectId || !projectId ||
|
|
11
12
|
identity.scopes?.some(scope => scope === 'tester' || scope === 'agent'))
|
|
12
13
|
return false;
|
|
@@ -14,10 +15,11 @@ export async function humanApprovalEligible(action, projectId, identity = reques
|
|
|
14
15
|
const source = approvalStore();
|
|
15
16
|
const client = typeof source.schema === 'function' ? source.schema('email') : source;
|
|
16
17
|
const { data: grant, error } = await client.from('mcp_oauth_tokens')
|
|
17
|
-
.select('id, user_id, connection_role, scopes, revoked_at, expires_at')
|
|
18
|
+
.select('id, user_id, connection_role, scopes, revoked_at, expires_at, chat_confirm_enabled')
|
|
18
19
|
.eq('id', identity.oauthTokenId).maybeSingle();
|
|
19
20
|
if (error || !grant || grant.id !== identity.oauthTokenId || grant.user_id !== identity.actorId ||
|
|
20
21
|
grant.connection_role !== null || grant.revoked_at !== null ||
|
|
22
|
+
(requireChatConfirm && grant.chat_confirm_enabled !== true) ||
|
|
21
23
|
!(Date.parse(grant.expires_at) > Date.now()) || !Array.isArray(grant.scopes) ||
|
|
22
24
|
grant.scopes.some((scope) => scope === 'tester' || scope === 'agent'))
|
|
23
25
|
return false;
|
|
@@ -37,3 +39,18 @@ export async function humanApprovalEligible(action, projectId, identity = reques
|
|
|
37
39
|
return false;
|
|
38
40
|
}
|
|
39
41
|
}
|
|
42
|
+
/** Opt-in is a live grant fact as well as a transport snapshot; errors never enable it. */
|
|
43
|
+
export async function chatConfirmConnectionEnabled(identity = requestContext()) {
|
|
44
|
+
if (identity?.chatConfirmEnabled !== true || !identity.oauthTokenId)
|
|
45
|
+
return false;
|
|
46
|
+
try {
|
|
47
|
+
const source = approvalStore();
|
|
48
|
+
const client = typeof source.schema === 'function' ? source.schema('email') : source;
|
|
49
|
+
const { data, error } = await client.from('mcp_oauth_tokens').select('chat_confirm_enabled')
|
|
50
|
+
.eq('id', identity.oauthTokenId).maybeSingle();
|
|
51
|
+
return !error && data?.chat_confirm_enabled === true;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
package/build/server.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface ToolRegistrationOptions {
|
|
|
4
4
|
scopes?: string[];
|
|
5
5
|
toolGroups?: string[];
|
|
6
6
|
toolAllowlist?: string[];
|
|
7
|
+
/** Server-owned connection opt-in; undefined only for unrestricted registry collectors. */
|
|
8
|
+
chatConfirmEnabled?: boolean;
|
|
7
9
|
}
|
|
8
10
|
declare const GROUP_REGISTRY: Record<string, (server: McpServer, options?: ToolRegistrationOptions) => void>;
|
|
9
11
|
export declare const ALL_TOOL_GROUPS: string[];
|
package/build/server.js
CHANGED
|
@@ -375,7 +375,7 @@ export function registerAllTools(server, options) {
|
|
|
375
375
|
if (rawScopes.includes('gateway')) {
|
|
376
376
|
const permissionScopes = rawScopes.filter((scope) => scope !== 'gateway');
|
|
377
377
|
const proxied = createScopeProxy(server, permissionScopes, 'gateway');
|
|
378
|
-
registerGatewayTools(proxied, { scopes: permissionScopes, toolGroups: options?.toolGroups, toolAllowlist: options?.toolAllowlist });
|
|
378
|
+
registerGatewayTools(proxied, { scopes: permissionScopes, toolGroups: options?.toolGroups, toolAllowlist: options?.toolAllowlist, chatConfirmEnabled: options?.chatConfirmEnabled === true });
|
|
379
379
|
return;
|
|
380
380
|
}
|
|
381
381
|
const groups = options?.toolGroups === undefined
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
|
|
2
|
+
import type { ToolRegistrationOptions } from '../server.js';
|
|
3
|
+
export declare function registerApprovalTools(server: McpServer, options?: ToolRegistrationOptions): void;
|
package/build/tools/approvals.js
CHANGED
|
@@ -2,9 +2,22 @@ import { z } from 'zod';
|
|
|
2
2
|
import { db } from '../db.js';
|
|
3
3
|
import { getProjectId } from '../project.js';
|
|
4
4
|
import { checkApproval } from '../lib/action-approvals.js';
|
|
5
|
+
import { confirmApproval, confirmApprovalInput } from '../lib/approval-chat-confirm.js';
|
|
5
6
|
const txt = (text) => ({ content: [{ type: 'text', text }] });
|
|
6
7
|
const j = (data) => txt(JSON.stringify(data, null, 2));
|
|
7
|
-
export function registerApprovalTools(server) {
|
|
8
|
+
export function registerApprovalTools(server, options) {
|
|
9
|
+
if (options?.chatConfirmEnabled !== false && (!options?.scopes || options.chatConfirmEnabled === true) &&
|
|
10
|
+
!options?.scopes?.some(scope => scope === 'tester' || scope === 'agent')) {
|
|
11
|
+
server.registerTool('confirm_approval', {
|
|
12
|
+
title: 'Confirm Approval',
|
|
13
|
+
description: "Records the user's yes. The user's own client shows a confirmation prompt. Never call this without asking the user first.",
|
|
14
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
15
|
+
inputSchema: confirmApprovalInput,
|
|
16
|
+
}, async (args) => {
|
|
17
|
+
const result = await confirmApproval(args);
|
|
18
|
+
return { ...j(result), ...('code' in result ? { isError: true } : {}) };
|
|
19
|
+
});
|
|
20
|
+
}
|
|
8
21
|
server.registerTool('check_approval', {
|
|
9
22
|
title: 'Check Approval',
|
|
10
23
|
annotations: {
|
package/build/tools/gateway.d.ts
CHANGED
package/build/tools/gateway.js
CHANGED
|
@@ -25,12 +25,12 @@ export function matchesToolQuery(entry, query) {
|
|
|
25
25
|
const SEARCH_LIMIT = 10;
|
|
26
26
|
async function buildCatalog(options) {
|
|
27
27
|
const innerServer = new McpServer({ name: 'sendinel-gateway-inner', version: '1.0.0' });
|
|
28
|
-
registerAllTools(innerServer, { scopes: options.scopes, toolGroups: options.toolGroups, toolAllowlist: options.toolAllowlist });
|
|
28
|
+
registerAllTools(innerServer, { scopes: options.scopes, toolGroups: options.toolGroups, toolAllowlist: options.toolAllowlist, chatConfirmEnabled: options.chatConfirmEnabled === true });
|
|
29
29
|
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
30
30
|
const client = new Client({ name: 'sendinel-gateway', version: '1.0.0' });
|
|
31
31
|
await innerServer.connect(serverTransport);
|
|
32
32
|
await client.connect(clientTransport);
|
|
33
|
-
const toolGroupMap = getRegisteredToolGroups({ scopes: options.scopes, toolGroups: options.toolGroups, toolAllowlist: options.toolAllowlist });
|
|
33
|
+
const toolGroupMap = getRegisteredToolGroups({ scopes: options.scopes, toolGroups: options.toolGroups, toolAllowlist: options.toolAllowlist, chatConfirmEnabled: options.chatConfirmEnabled === true });
|
|
34
34
|
const { tools } = await client.listTools();
|
|
35
35
|
const entries = tools
|
|
36
36
|
.map((tool) => ({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.126",
|
|
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",
|