@sendinel/mcp-server 1.0.224 → 1.0.226

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.
@@ -35,6 +35,12 @@ export declare function toolError(body: unknown): {
35
35
  */
36
36
  export declare class PlainToolRefusal extends Error {
37
37
  }
38
+ /**
39
+ * Refusal class for plain refusals (#4811). The text stays the bare sentence, so the class rides in
40
+ * `_meta.error_code`: it survives the in-process MCP client hop behind invoke_tool, which clones the
41
+ * result and loses WeakSet identity. Foreign, missing and malformed ids share it (no existence oracle).
42
+ */
43
+ export declare const PLAIN_REFUSAL_ERROR_CODE: ToolErrorCode;
38
44
  /** The refusal as tool text with isError, exempt from JSON re-coding by codedToolResult. */
39
45
  export declare function plainRefusalResult(message: string): {
40
46
  isError: true;
@@ -42,6 +48,9 @@ export declare function plainRefusalResult(message: string): {
42
48
  type: 'text';
43
49
  text: string;
44
50
  }[];
51
+ _meta: {
52
+ error_code: "conflict" | "forbidden" | "internal_error" | "not_found" | "plan_limit" | "validation_failed";
53
+ };
45
54
  };
46
55
  /** A table known to block a delete. `count` is best-effort; it may throw or return null. */
47
56
  export interface DeleteBlocker {
@@ -55,9 +55,23 @@ export function toolError(body) {
55
55
  export class PlainToolRefusal extends Error {
56
56
  }
57
57
  const plainRefusalResults = new WeakSet();
58
+ /**
59
+ * Refusal class for plain refusals (#4811). The text stays the bare sentence, so the class rides in
60
+ * `_meta.error_code`: it survives the in-process MCP client hop behind invoke_tool, which clones the
61
+ * result and loses WeakSet identity. Foreign, missing and malformed ids share it (no existence oracle).
62
+ */
63
+ export const PLAIN_REFUSAL_ERROR_CODE = 'not_found';
64
+ function hasPlainRefusalMeta(result) {
65
+ const meta = result?._meta;
66
+ return meta?.error_code === PLAIN_REFUSAL_ERROR_CODE;
67
+ }
58
68
  /** The refusal as tool text with isError, exempt from JSON re-coding by codedToolResult. */
59
69
  export function plainRefusalResult(message) {
60
- const result = { isError: true, content: [{ type: 'text', text: message }] };
70
+ const result = {
71
+ isError: true,
72
+ content: [{ type: 'text', text: message }],
73
+ _meta: { error_code: PLAIN_REFUSAL_ERROR_CODE },
74
+ };
61
75
  plainRefusalResults.add(result);
62
76
  return result;
63
77
  }
@@ -253,7 +267,7 @@ export function toolErrorClass(body) {
253
267
  }
254
268
  /** Add a stable class to refusals without dropping existing fields or success data. */
255
269
  export function codedToolResult(result) {
256
- if (plainRefusalResults.has(result))
270
+ if (plainRefusalResults.has(result) || (result.isError === true && hasPlainRefusalMeta(result)))
257
271
  return result;
258
272
  let failed = result.isError === true;
259
273
  const content = result.content.map(block => {
@@ -1,4 +1,4 @@
1
- import { PLAN_LIMITS as DOMAIN_PLAN_LIMITS, readPlanUsage, readAllRows } from "@sendinel/domain";
1
+ import { PLAN_LIMITS as DOMAIN_PLAN_LIMITS, readPlanUsage, readAiTokenUsage, readAllRows } from "@sendinel/domain";
2
2
  import { z } from 'zod';
3
3
  import { logAudit } from '../audit.js';
4
4
  import { adminDb, db } from '../db.js';
@@ -561,20 +561,21 @@ export function registerOrgTools(server) {
561
561
  idempotentHint: true,
562
562
  openWorldHint: false,
563
563
  },
564
- description: 'Get current plan usage for the project.',
564
+ description: 'Get current plan usage for the project, including the monthly AI token allowance.',
565
565
  inputSchema: {},
566
566
  }, async () => {
567
567
  const projectId = getProjectId();
568
568
  const { data: project } = await db().from('projects').select('plan').eq('id', projectId).maybeSingle();
569
569
  const plan = project?.plan ?? 'free';
570
570
  const limits = PLAN_LIMITS[plan] ?? PLAN_LIMITS.free ?? {};
571
- const [contacts, sends, drafts, { count: sites }] = await Promise.all([
571
+ const [contacts, sends, drafts, aiTokens, { count: sites }] = await Promise.all([
572
572
  readPlanUsage(db(), projectId, "contacts"),
573
573
  readPlanUsage(db(), projectId, "monthly_sends"),
574
574
  checkPlanLimit('ai_drafts').then((result) => ({ data: result, error: null })).catch((error) => ({
575
575
  data: null,
576
576
  error: error instanceof Error ? error.message : 'Unable to read draft usage',
577
577
  })),
578
+ readAiTokenUsage(db(), projectId),
578
579
  db().from('sites').select('id', { count: 'exact', head: true }).eq('project_id', projectId),
579
580
  ]);
580
581
  if (contacts.error || sends.error || contacts.data?.error || sends.data?.error || !contacts.data || !sends.data) {
@@ -588,6 +589,7 @@ export function registerOrgTools(server) {
588
589
  ai_drafts: typeof drafts.data?.current === 'number'
589
590
  ? { used: drafts.data.current, limit: limits.ai_drafts ?? null }
590
591
  : { used: null, limit: limits.ai_drafts ?? null, reason: 'Unable to read draft usage' },
592
+ ai_tokens: aiTokens ?? { used: null, limit: null, reason: 'Unable to read AI token usage' },
591
593
  log_retention_days: limits.log_retention_days ?? null,
592
594
  });
593
595
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendinel/mcp-server",
3
- "version": "1.0.224",
3
+ "version": "1.0.226",
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",