@sendinel/mcp-server 1.0.67 → 1.0.69

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.
@@ -5,16 +5,23 @@
5
5
  // than a cross-package import — keep both in sync if the SSRF guard changes.
6
6
  import dns from 'node:dns/promises';
7
7
  import net from 'node:net';
8
- import { Agent } from 'undici';
8
+ // #4049: fetch must come from the same undici as Agent. Node's global fetch bundles its own undici and
9
+ // rejects a foreign dispatcher (UND_ERR_INVALID_ARG) before any request, so every pinned fetch failed.
10
+ import { Agent, fetch as undiciFetch } from 'undici';
9
11
  export function createPinnedIpDispatcher(hostname, resolved) {
10
12
  const expectedHost = hostname.toLowerCase();
11
13
  return new Agent({
12
14
  connect: {
13
- lookup(requestedHost, _options, callback) {
15
+ lookup(requestedHost, options, callback) {
14
16
  if (String(requestedHost).toLowerCase() !== expectedHost) {
15
17
  callback(new Error('Pinned fetch attempted to resolve an unexpected host'), []);
16
18
  return;
17
19
  }
20
+ // Node's connect asks for every address when options.all is set; answer in the shape it asked for.
21
+ if (options?.all) {
22
+ callback(null, [{ address: resolved.address, family: resolved.family }]);
23
+ return;
24
+ }
18
25
  callback(null, resolved.address, resolved.family);
19
26
  },
20
27
  },
@@ -23,7 +30,7 @@ export function createPinnedIpDispatcher(hostname, resolved) {
23
30
  }
24
31
  export function fetchPinnedToResolvedIp(url, resolved, init = {}) {
25
32
  const dispatcher = createPinnedIpDispatcher(url.hostname, resolved);
26
- return fetch(url, { ...init, dispatcher });
33
+ return undiciFetch(url, { ...init, dispatcher });
27
34
  }
28
35
  function isPrivateIp(address) {
29
36
  if (net.isIPv4(address)) {
package/build/server.d.ts CHANGED
@@ -16,6 +16,17 @@ export declare const TESTER_CONNECTION_SCOPE = "tester";
16
16
  export declare function isTesterConnection(scopes: readonly string[] | null | undefined): boolean;
17
17
  /** Tools a tester connection may see and call: reads, plus anything that goes through a person's approval. */
18
18
  export declare function testerConnectionAllowsTool(name: string): boolean;
19
+ /**
20
+ * Tools whose own input already has a top-level `project_id` that means something other than "the project
21
+ * to act on" (#4033). The per-call project override never reads, strips or checks it for these.
22
+ * A test keeps this list equal to the registered schemas.
23
+ */
24
+ export declare const PROJECT_ID_NATIVE_TOOLS: ReadonlySet<string>;
25
+ /**
26
+ * Tools that need a stated project when the connection is switched away from its home project (#4033):
27
+ * anything that goes through a person's approval, plus the two always-gated actions.
28
+ */
29
+ export declare function isProjectPinnedWriteTool(name: string): boolean;
19
30
  export declare function testerConnectionRefusal(name: string): string;
20
31
  export declare const GATEWAY_TOOL_NAMES: string[];
21
32
  /**
package/build/server.js CHANGED
@@ -191,6 +191,19 @@ export function testerConnectionAllowsTool(name) {
191
191
  || name === 'activate_campaigns'
192
192
  || isApprovalGatedAction(name);
193
193
  }
194
+ /**
195
+ * Tools whose own input already has a top-level `project_id` that means something other than "the project
196
+ * to act on" (#4033). The per-call project override never reads, strips or checks it for these.
197
+ * A test keeps this list equal to the registered schemas.
198
+ */
199
+ export const PROJECT_ID_NATIVE_TOOLS = new Set(['set_active_project', 'get_outbound_digest']);
200
+ /**
201
+ * Tools that need a stated project when the connection is switched away from its home project (#4033):
202
+ * anything that goes through a person's approval, plus the two always-gated actions.
203
+ */
204
+ export function isProjectPinnedWriteTool(name) {
205
+ return name === 'send_broadcast' || name === 'activate_campaigns' || isApprovalGatedAction(name);
206
+ }
194
207
  export function testerConnectionRefusal(name) {
195
208
  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
209
  }
@@ -337,7 +350,7 @@ export const SENDINEL_SERVER_INSTRUCTIONS = [
337
350
  'Sendinel runs email for this workspace: contacts, segments, campaigns, templates and sending.',
338
351
  'To send a one-off email, use send_broadcast: call it with an audience and a brief (or html + subject), show the person the subject, recipient count and approve_url it returns, then call it again with only approval_id once they have approved.',
339
352
  'Anything that emails real people needs a person to approve it. Never say an email was sent until the tool that sends it has succeeded.',
340
- 'In gateway mode, find tools with search_tools (plain words work, for example "send email" or "last campaign"), check arguments with describe_tool, then call invoke_tool.',
353
+ 'On the hosted connection, any tool call (and invoke_tool) also accepts an optional project_id to act on one project for that call only; it is checked each time, and approval-gated tools require it when the connection was switched to another project. In gateway mode, find tools with search_tools (plain words work, for example "send email" or "last campaign"), check arguments with describe_tool, then call invoke_tool.',
341
354
  ].join(' ');
342
355
  export function registerAllTools(server, options) {
343
356
  const rawScopes = options?.scopes ?? [];
@@ -453,7 +453,7 @@ export function registerAnalyticsTools(server) {
453
453
  idempotentHint: true,
454
454
  openWorldHint: false,
455
455
  },
456
- description: 'Switch this connection to act on a different project you have access to, without reconnecting. Only available on an OAuth-connected session (claude.ai / Claude Code / Cursor); API keys and Synchronex connections stay pinned to their original project.',
456
+ description: 'Switch this connection to act on a different project you have access to, without reconnecting. Only available on an OAuth-connected session (claude.ai / Claude Code / Cursor); API keys and Synchronex connections stay pinned to their original project. This switch is shared by every session using the connection; to act on one project without changing it, pass project_id on that tool call instead.',
457
457
  inputSchema: {
458
458
  project_id: z.string().uuid().describe('Project UUID to switch to. Use list_projects to see what you can access.'),
459
459
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendinel/mcp-server",
3
- "version": "1.0.67",
3
+ "version": "1.0.69",
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",