@sendinel/mcp-server 1.0.64 → 1.0.66

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/server.d.ts CHANGED
@@ -8,6 +8,15 @@ export declare const ALL_TOOL_GROUPS: string[];
8
8
  export type ToolGroup = keyof typeof GROUP_REGISTRY;
9
9
  export type ToolGroupMap = Record<string, string>;
10
10
  export declare function getRequiredScope(name: string): string;
11
+ /**
12
+ * Marker scope for a tester connection (#4011), a new OAuth grant until an owner promotes it. It adds no
13
+ * permission level: such a connection sees every read tool plus only the approval-gated tools.
14
+ */
15
+ export declare const TESTER_CONNECTION_SCOPE = "tester";
16
+ export declare function isTesterConnection(scopes: readonly string[] | null | undefined): boolean;
17
+ /** Tools a tester connection may see and call: reads, plus anything that goes through a person's approval. */
18
+ export declare function testerConnectionAllowsTool(name: string): boolean;
19
+ export declare function testerConnectionRefusal(name: string): string;
11
20
  export declare const GATEWAY_TOOL_NAMES: string[];
12
21
  /**
13
22
  * Collects tool-name → group metadata from the same group registry used by
package/build/server.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { runWithToolContext } from './db.js';
2
+ import { isApprovalGatedAction } from './lib/action-approvals.js';
2
3
  import { registerAnalyticsTools } from './tools/analytics.js';
3
4
  import { registerCampaignTools } from './tools/campaigns.js';
4
5
  import { registerCampaignSetTools } from './tools/campaign-sets.js';
@@ -175,6 +176,24 @@ function scopeAllows(keyScopes, required) {
175
176
  const requiredLevel = SCOPE_HIERARCHY[required] ?? 99;
176
177
  return keyScopes.some((s) => (SCOPE_HIERARCHY[s] ?? 0) >= requiredLevel);
177
178
  }
179
+ /**
180
+ * Marker scope for a tester connection (#4011), a new OAuth grant until an owner promotes it. It adds no
181
+ * permission level: such a connection sees every read tool plus only the approval-gated tools.
182
+ */
183
+ export const TESTER_CONNECTION_SCOPE = 'tester';
184
+ export function isTesterConnection(scopes) {
185
+ return !!scopes?.includes(TESTER_CONNECTION_SCOPE);
186
+ }
187
+ /** Tools a tester connection may see and call: reads, plus anything that goes through a person's approval. */
188
+ export function testerConnectionAllowsTool(name) {
189
+ return getRequiredScope(name) === 'read'
190
+ || name === 'send_broadcast'
191
+ || name === 'activate_campaigns'
192
+ || isApprovalGatedAction(name);
193
+ }
194
+ export function testerConnectionRefusal(name) {
195
+ return `"${name}" is not available to a tester connection. A tester connection can read, and can ask a person to approve outward actions: call an approval-gated tool (for example send_broadcast) and a person approves it from the approve_url it returns. To change this tool directly, ask the workspace owner to promote the connection in Dashboard > Agent Connect.`;
196
+ }
178
197
  // Names of the meta-tools gateway mode registers in place of the real catalog
179
198
  // (see tools/gateway.ts). Grouped as 'gateway' rather than any real tool
180
199
  // group -- a gateway-mode client's toolGroups list holds the groups it can
@@ -285,14 +304,19 @@ function createScopeProxy(server, scopes, toolGroup) {
285
304
  // ── Scope enforcement (only when scopes are provided) ─────────────
286
305
  if (scopes !== null) {
287
306
  const required = getRequiredScope(name);
288
- if (!scopeAllows(scopes, required)) {
307
+ const tester = isTesterConnection(scopes);
308
+ const allowed = () => tester
309
+ ? testerConnectionAllowsTool(name)
310
+ : scopeAllows(scopes, required);
311
+ if (!allowed()) {
289
312
  // Don't register tools the key can't access — they won't appear in tool list
290
313
  return;
291
314
  }
292
315
  // Wrap handler with runtime scope check (defense in depth)
293
316
  const guardedHandler = (...args) => {
294
- if (!scopeAllows(scopes, required)) {
295
- return { content: [{ type: 'text', text: `Forbidden: this action requires "${required}" scope.` }] };
317
+ if (!allowed()) {
318
+ const text = tester ? testerConnectionRefusal(name) : `Forbidden: this action requires "${required}" scope.`;
319
+ return { content: [{ type: 'text', text }] };
296
320
  }
297
321
  return runWithToolContext(toolGroup, name, () => handler(...args));
298
322
  };
@@ -3140,6 +3140,16 @@ Return ONLY valid JSON with this exact shape:
3140
3140
  survey_id: z.string().uuid().optional(),
3141
3141
  skip_if_event: z.string().trim().min(1).nullable().optional().describe('Event name that skips this step at send time; null clears the rule.'),
3142
3142
  send_only_if_event: z.string().trim().min(1).nullable().optional().describe('Event required for this step to send; null clears the rule.'),
3143
+ link_actions: z.array(z.discriminatedUnion('action', [
3144
+ z.object({ url_pattern: z.string().trim().min(1).max(500), action: z.enum(['add_tag', 'remove_tag']), tag: z.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/) }),
3145
+ z.object({
3146
+ url_pattern: z.string().trim().min(1).max(500).describe('Placeholder URL in the email body (a trailing * matches a prefix). Matching links are rewritten at send time to a signed /api/pick/<token> confirm page.'),
3147
+ action: z.literal('set_property'),
3148
+ property: z.string().regex(/^[a-z][a-z0-9_]{0,62}$/).describe('Custom contact property to set, for example "track". Consent, suppression and unsubscribe fields are rejected.'),
3149
+ value: z.string().trim().min(1).max(100),
3150
+ continue_url: z.string().url().max(500).optional().describe('Optional http(s) link shown on the thank-you page.'),
3151
+ }),
3152
+ ])).max(10).optional().describe('Link actions for this step. set_property links show a Confirm page; only the Confirm click writes the property (last click wins). Replaces the whole list.'),
3143
3153
  },
3144
3154
  }, async ({ step_id, ...updates }) => {
3145
3155
  const projectId = getProjectId();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendinel/mcp-server",
3
- "version": "1.0.64",
3
+ "version": "1.0.66",
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",