frinus-mcp 3.6.4 → 3.7.0

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.
@@ -1,4 +1,4 @@
1
- import type { CpClientInterface } from "../types/index.js";
1
+ import type { CpClientInterface, NotificationInbox } from "../types/index.js";
2
2
  export declare class CpClient implements CpClientInterface {
3
3
  private client;
4
4
  constructor(baseURL: string, apiKey: string);
@@ -13,6 +13,12 @@ export declare class CpClient implements CpClientInterface {
13
13
  description?: string;
14
14
  }): Promise<any>;
15
15
  deleteUniverse(orgId: string, universeId: string): Promise<void>;
16
+ getNotificationInbox(opts?: {
17
+ channel?: string;
18
+ limit?: number;
19
+ markDelivered?: boolean;
20
+ }): Promise<NotificationInbox>;
21
+ ackNotification(id: string): Promise<void>;
16
22
  storeCredential(integrationRef: string, data: Record<string, unknown>, metadata?: Record<string, unknown>): Promise<any>;
17
23
  getCredential(integrationRef: string): Promise<any>;
18
24
  listCredentials(): Promise<any>;
@@ -4,6 +4,13 @@
4
4
  import axios from "axios";
5
5
  import { getResolvedTenantOrgId } from "./memory-client.js";
6
6
  import { getResolvedClient } from "./client-info.js";
7
+ /**
8
+ * Short timeout (ms) for the platform-notification calls. These are best-effort
9
+ * side channels that must never slow down bootstrap/heartbeat or hold the agent
10
+ * hostage on a slow network, so they use a much tighter deadline than the 30s
11
+ * default of the rest of the Control Plane client.
12
+ */
13
+ const NOTIFICATION_TIMEOUT_MS = 4000;
7
14
  export class CpClient {
8
15
  client;
9
16
  constructor(baseURL, apiKey) {
@@ -37,6 +44,25 @@ export class CpClient {
37
44
  await this.client.delete(`/api/v1/orgs/${orgId}/universes/${universeId}`);
38
45
  }
39
46
  // =========================================================================
47
+ // Platform Notifications (short timeout, best-effort)
48
+ // =========================================================================
49
+ async getNotificationInbox(opts = {}) {
50
+ const response = await this.client.get(`/api/v1/notifications/inbox`, {
51
+ params: {
52
+ channel: opts.channel ?? "mcp",
53
+ limit: opts.limit ?? 3,
54
+ mark_delivered: opts.markDelivered ?? false,
55
+ },
56
+ timeout: NOTIFICATION_TIMEOUT_MS,
57
+ });
58
+ return response.data;
59
+ }
60
+ async ackNotification(id) {
61
+ await this.client.post(`/api/v1/notifications/${id}/ack`, undefined, {
62
+ timeout: NOTIFICATION_TIMEOUT_MS,
63
+ });
64
+ }
65
+ // =========================================================================
40
66
  // Credential Vault
41
67
  // =========================================================================
42
68
  async storeCredential(integrationRef, data, metadata) {
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@
11
11
  * tools/handlers.ts - Tool handlers (dispatch map)
12
12
  * types/index.ts - Shared interfaces and types
13
13
  */
14
+ import net from "node:net";
14
15
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
15
16
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
17
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
@@ -26,6 +27,15 @@ import { dispatchTool } from "./tools/handlers.js";
26
27
  const MEMORY_SERVICE_URL = process.env.MEMORY_SERVICE_URL || "https://frinus-memory.rdxsec.com.br";
27
28
  const CP_URL = process.env.FRINUS_CP_URL || "https://frinus-api.rdxsec.com.br";
28
29
  const AGENT_SERVICE_URL = process.env.AGENT_SERVICE_URL || "https://frinus-agents.rdxsec.com.br";
30
+ // Give Happy Eyeballs (RFC 8305) a realistic deadline before giving up on an
31
+ // address family. Node's default auto-select-family attempt timeout is 250ms,
32
+ // which on a slow network (2026-10-27 incident) made the API-key validation
33
+ // fail with "Failed to validate API key: AggregateError" before the TCP
34
+ // handshake could complete. 5s per attempt matches how long a cold mobile/VPN
35
+ // link realistically needs. Guarded with typeof for older Node runtimes.
36
+ if (typeof net.setDefaultAutoSelectFamilyAttemptTimeout === "function") {
37
+ net.setDefaultAutoSelectFamilyAttemptTimeout(5000);
38
+ }
29
39
  // Main server
30
40
  async function main() {
31
41
  // Require API key (FRINUS_API_KEY preferred, FRINUS_MEMORY_API_KEY for backward compat)
@@ -40,31 +50,52 @@ async function main() {
40
50
  const cpClient = new CpClient(CP_URL, apiKey);
41
51
  const capture = new InteractionCapture(memoryClient);
42
52
  console.error(`[InteractionCapture] Session: ${capture.getSessionId()}`);
43
- // Validate API key and resolve tenant identity at startup
44
- try {
45
- const me = await memoryClient.getAuthMe();
46
- setResolvedUserEmail(me.email || me.user?.email || null);
47
- setResolvedUserId(me.id || me.user?.id || me.user_id || null);
48
- // Resolve tenant org ID from /auth/me response (env var takes precedence)
49
- const meOrgId = me.organization_id || me.org_id || me.user?.organization_id || null;
50
- if (!getResolvedTenantOrgId() && meOrgId) {
51
- setResolvedTenantOrgId(meOrgId);
52
- }
53
- console.error(`[Auth] API key validated. User: ${getResolvedUserEmail()}, Tenant: ${getResolvedTenantOrgId() || 'none'}`);
54
- }
55
- catch (err) {
56
- const status = err?.response?.status;
57
- if (status === 401 || status === 403) {
58
- console.error(`[FATAL] API key rejected (HTTP ${status}). Check FRINUS_API_KEY.`);
53
+ // Validate API key and resolve tenant identity at startup.
54
+ //
55
+ // Retry ONLY transient network failures (no HTTP response: DNS, connection
56
+ // refused/reset, timeout, AggregateError from Happy Eyeballs). A 401/403 is a
57
+ // real rejection and must fail fast — retrying a bad key is pointless. Backoff
58
+ // 1s then 3s between the 3 attempts. This, together with the auto-select-family
59
+ // timeout above, is the fix for the 2026-10-27 "Failed to validate API key:
60
+ // AggregateError" crashes on slow links.
61
+ const MAX_AUTH_ATTEMPTS = 3;
62
+ const AUTH_BACKOFFS_MS = [1000, 3000];
63
+ let me;
64
+ for (let attempt = 1;; attempt++) {
65
+ try {
66
+ me = await memoryClient.getAuthMe();
67
+ break;
59
68
  }
60
- else {
69
+ catch (err) {
70
+ const status = err?.response?.status;
71
+ if (status === 401 || status === 403) {
72
+ console.error(`[FATAL] API key rejected (HTTP ${status}). Check FRINUS_API_KEY.`);
73
+ process.exit(1);
74
+ }
75
+ // No HTTP response == network/transport error -> retryable.
76
+ const isNetworkError = status === undefined;
77
+ if (isNetworkError && attempt < MAX_AUTH_ATTEMPTS) {
78
+ const waitMs = AUTH_BACKOFFS_MS[attempt - 1];
79
+ console.error(`[Auth] Network error validating API key (attempt ${attempt}/${MAX_AUTH_ATTEMPTS}): ` +
80
+ `${err?.message || err}. Retrying in ${waitMs}ms...`);
81
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
82
+ continue;
83
+ }
61
84
  console.error(`[FATAL] Failed to validate API key: ${err?.message || err}`);
85
+ process.exit(1);
62
86
  }
63
- process.exit(1);
64
87
  }
88
+ setResolvedUserEmail(me.email || me.user?.email || null);
89
+ setResolvedUserId(me.id || me.user?.id || me.user_id || null);
90
+ // Resolve tenant org ID from /auth/me response (env var takes precedence)
91
+ const meOrgId = me.organization_id || me.org_id || me.user?.organization_id || null;
92
+ if (!getResolvedTenantOrgId() && meOrgId) {
93
+ setResolvedTenantOrgId(meOrgId);
94
+ }
95
+ console.error(`[Auth] API key validated. User: ${getResolvedUserEmail()}, Tenant: ${getResolvedTenantOrgId() || 'none'}`);
65
96
  const server = new Server({
66
97
  name: "frinus",
67
- version: "3.6.4",
98
+ version: "3.7.0",
68
99
  }, {
69
100
  capabilities: {
70
101
  tools: {},
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Platform notifications delivered through the MCP channel.
3
+ *
4
+ * Clients rarely come back to the web dashboard after the initial integration,
5
+ * so the MCP session itself becomes the way to reach them. This module fetches
6
+ * the Control Plane inbox and renders a compact markdown block that is appended
7
+ * to `agent_bootstrap` (once, forced) and `heartbeat_tick` (throttled).
8
+ *
9
+ * Design rules:
10
+ * - NEVER throws. A 404, timeout or any network error yields '' plus a stderr
11
+ * log. The agent's real work must never be blocked by the news channel.
12
+ * - In-process state, per MCP process (= per session): which notification ids
13
+ * were already shown, and when we last hit the inbox. A long-lived process
14
+ * therefore shows each item at most once and polls at most once per window.
15
+ * - Env toggles: FRINUS_NOTIFICATIONS=off disables everything;
16
+ * FRINUS_NOTIFICATIONS_INTERVAL_MIN overrides the throttle window (default 60).
17
+ */
18
+ import type { CpClientInterface } from "./types/index.js";
19
+ /** Reset module state. Test-only; not part of the runtime surface. */
20
+ export declare function __resetNotificationsState(): void;
21
+ export interface FetchBlockOptions {
22
+ /** Bypass the throttle window (used by agent_bootstrap). */
23
+ force?: boolean;
24
+ }
25
+ /**
26
+ * Fetch the inbox and render the "Novidades do Frinus" block, or '' when there
27
+ * is nothing new to show (disabled, throttled, empty, already-shown, or any
28
+ * failure). Guaranteed not to throw.
29
+ */
30
+ export declare function fetchNotificationsBlock(cpClient: CpClientInterface, opts?: FetchBlockOptions): Promise<string>;
@@ -0,0 +1,86 @@
1
+ /** Max characters of a notification body rendered inline. */
2
+ const BODY_LIMIT = 600;
3
+ /** Default throttle window for non-forced checks (minutes). */
4
+ const DEFAULT_INTERVAL_MIN = 60;
5
+ // ---------------------------------------------------------------------------
6
+ // In-process session state
7
+ // ---------------------------------------------------------------------------
8
+ /** Notification ids already shown in this session (dedupe across bootstrap + ticks). */
9
+ const shownIds = new Set();
10
+ /** Epoch ms of the last inbox check (forced or throttled). 0 = never. */
11
+ let lastCheckAt = 0;
12
+ /** Reset module state. Test-only; not part of the runtime surface. */
13
+ export function __resetNotificationsState() {
14
+ shownIds.clear();
15
+ lastCheckAt = 0;
16
+ }
17
+ function isDisabled() {
18
+ return (process.env.FRINUS_NOTIFICATIONS || "").trim().toLowerCase() === "off";
19
+ }
20
+ function throttleMs() {
21
+ const raw = process.env.FRINUS_NOTIFICATIONS_INTERVAL_MIN;
22
+ const parsed = raw !== undefined ? Number.parseInt(raw, 10) : NaN;
23
+ const minutes = Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_INTERVAL_MIN;
24
+ return minutes * 60_000;
25
+ }
26
+ /**
27
+ * Fetch the inbox and render the "Novidades do Frinus" block, or '' when there
28
+ * is nothing new to show (disabled, throttled, empty, already-shown, or any
29
+ * failure). Guaranteed not to throw.
30
+ */
31
+ export async function fetchNotificationsBlock(cpClient, opts = {}) {
32
+ if (isDisabled())
33
+ return "";
34
+ const now = Date.now();
35
+ if (!opts.force && now - lastCheckAt < throttleMs()) {
36
+ return "";
37
+ }
38
+ lastCheckAt = now;
39
+ let inbox;
40
+ try {
41
+ inbox = await cpClient.getNotificationInbox({
42
+ channel: "mcp",
43
+ limit: 3,
44
+ markDelivered: true,
45
+ });
46
+ }
47
+ catch (err) {
48
+ // 404 (CP not yet deployed), timeout, DNS, connection refused — all benign
49
+ // here. Swallow and keep working; log to stderr for operators.
50
+ console.error(`[Notifications] inbox fetch failed: ${err?.message || err}`);
51
+ return "";
52
+ }
53
+ const fresh = (inbox?.items || []).filter((it) => !!it && typeof it.id === "string" && !shownIds.has(it.id));
54
+ if (fresh.length === 0)
55
+ return "";
56
+ for (const it of fresh)
57
+ shownIds.add(it.id);
58
+ return renderBlock(fresh);
59
+ }
60
+ /** Render the markdown block the agent relays to the user. */
61
+ function renderBlock(items) {
62
+ let out = `## 📣 Novidades do Frinus\n\n`;
63
+ for (const it of items) {
64
+ const sev = it.severity ? ` _(${it.severity})_` : "";
65
+ out += `### ${it.title}${sev}\n`;
66
+ let body = (it.body || "").trim();
67
+ if (body.length > BODY_LIMIT) {
68
+ body = body.slice(0, BODY_LIMIT).trimEnd() + "…";
69
+ }
70
+ if (body)
71
+ out += `${body}\n`;
72
+ if (it.cta_label && it.cta_url) {
73
+ out += `\n[${it.cta_label}](${it.cta_url})\n`;
74
+ }
75
+ else if (it.cta_url) {
76
+ out += `\n${it.cta_url}\n`;
77
+ }
78
+ out += `\n_id: ${it.id}_\n\n`;
79
+ }
80
+ out += `---\n`;
81
+ out +=
82
+ `Avise o usuário destas novidades de forma breve (1–3 linhas cada) no ` +
83
+ `início da sua próxima resposta, sem interromper a tarefa. Quando ele ` +
84
+ `confirmar ciência ou pedir para não ver mais, chame notification_ack(id).\n`;
85
+ return out;
86
+ }
@@ -1855,4 +1855,43 @@ granted it and when. No secret data.`,
1855
1855
  required: ["ref"],
1856
1856
  },
1857
1857
  },
1858
+ // ==========================================================================
1859
+ // Platform Notifications (2)
1860
+ // ==========================================================================
1861
+ {
1862
+ name: "notifications_list",
1863
+ description: `List pending Frinus platform notifications for the current user.
1864
+
1865
+ These are platform-authored announcements: new features, maintenance windows,
1866
+ tips and alerts. This is a READ-ONLY peek — it does not mark anything as
1867
+ delivered or acknowledged, so items stay in the inbox until the user sees them.
1868
+
1869
+ Each item includes an \`id\` you can pass to notification_ack once the user has
1870
+ read it. Use this when the user asks "what's new" or to re-check news that was
1871
+ surfaced earlier in the session.`,
1872
+ inputSchema: {
1873
+ type: "object",
1874
+ properties: {},
1875
+ required: [],
1876
+ },
1877
+ },
1878
+ {
1879
+ name: "notification_ack",
1880
+ description: `Acknowledge a Frinus platform notification by id.
1881
+
1882
+ Call this once the user confirms they have seen the news, or asks not to see it
1883
+ again. The notification then disappears from the inbox and will not be shown
1884
+ again (in this or future sessions). The id comes from the "📣 Novidades do
1885
+ Frinus" block or from notifications_list.`,
1886
+ inputSchema: {
1887
+ type: "object",
1888
+ properties: {
1889
+ id: {
1890
+ type: "string",
1891
+ description: "UUID of the notification to acknowledge (required)",
1892
+ },
1893
+ },
1894
+ required: ["id"],
1895
+ },
1896
+ },
1858
1897
  ];
@@ -7,6 +7,7 @@
7
7
  import axios from "axios";
8
8
  import { spawn } from "node:child_process";
9
9
  import { getResolvedTenantOrgId } from "../client/memory-client.js";
10
+ import { fetchNotificationsBlock } from "../notifications.js";
10
11
  // ---------------------------------------------------------------------------
11
12
  // Credential delivery — server-side broker, never by value
12
13
  // ---------------------------------------------------------------------------
@@ -982,7 +983,7 @@ const handlers = {
982
983
  // ==========================================================================
983
984
  // Maintenance
984
985
  // ==========================================================================
985
- async heartbeat_tick(args, { memoryClient }) {
986
+ async heartbeat_tick(args, { memoryClient, cpClient }) {
986
987
  const result = await memoryClient.heartbeatTick({
987
988
  agent_id: args.agent_id,
988
989
  context_id: args.context_id,
@@ -1012,6 +1013,13 @@ const handlers = {
1012
1013
  formatted += ` ${action}: at interaction ${count}\n`;
1013
1014
  }
1014
1015
  }
1016
+ // Piggyback the notifications check on the heartbeat. fetchNotificationsBlock
1017
+ // self-throttles (at most once per FRINUS_NOTIFICATIONS_INTERVAL_MIN) and
1018
+ // dedupes against what this session already showed, so this is cheap and
1019
+ // never shows the same item twice.
1020
+ const notif = await fetchNotificationsBlock(cpClient, { force: false });
1021
+ if (notif)
1022
+ formatted += `\n${notif}`;
1015
1023
  return { content: [{ type: "text", text: formatted }] };
1016
1024
  },
1017
1025
  async sleep_run(args, { memoryClient }) {
@@ -1246,7 +1254,12 @@ const handlers = {
1246
1254
  }],
1247
1255
  };
1248
1256
  },
1249
- async agent_bootstrap(args, { agentClient }) {
1257
+ async agent_bootstrap(args, { agentClient, cpClient }) {
1258
+ // Kick off the notifications fetch concurrently with the bootstrap call so
1259
+ // it adds no latency on the critical path. fetchNotificationsBlock never
1260
+ // throws and self-limits to a 4s timeout, so bootstrap can neither fail nor
1261
+ // stall because of it; the extra .catch is defence in depth.
1262
+ const notifPromise = fetchNotificationsBlock(cpClient, { force: true }).catch(() => "");
1250
1263
  try {
1251
1264
  const result = await agentClient.bootstrapAgent(args.agent_id);
1252
1265
  const identity = result.identity || {};
@@ -1310,6 +1323,9 @@ const handlers = {
1310
1323
  if (delegateDigest && delegateDigest.count > 0 && delegateDigest.summary) {
1311
1324
  formatted += `\n### Delegate Targets\n${delegateDigest.summary}\n`;
1312
1325
  }
1326
+ const notif = await notifPromise;
1327
+ if (notif)
1328
+ formatted += `\n\n${notif}`;
1313
1329
  return { content: [{ type: "text", text: formatted }] };
1314
1330
  }
1315
1331
  catch (error) {
@@ -2316,6 +2332,81 @@ const handlers = {
2316
2332
  throw error;
2317
2333
  }
2318
2334
  },
2335
+ // ==========================================================================
2336
+ // Platform Notifications
2337
+ // ==========================================================================
2338
+ async notifications_list(_args, { cpClient }) {
2339
+ try {
2340
+ // mark_delivered=false: this is a read-only peek that must NOT consume a
2341
+ // delivery against max_deliveries. Use a higher limit than the automatic
2342
+ // channels so the user can see everything still pending.
2343
+ const inbox = await cpClient.getNotificationInbox({
2344
+ channel: "mcp",
2345
+ limit: 10,
2346
+ markDelivered: false,
2347
+ });
2348
+ const items = inbox?.items || [];
2349
+ if (items.length === 0) {
2350
+ return { content: [{ type: "text", text: "No pending Frinus notifications." }] };
2351
+ }
2352
+ let text = `${items.length} pending Frinus notification${items.length === 1 ? "" : "s"}:\n\n`;
2353
+ items.forEach((it) => {
2354
+ const sev = it.severity ? `[${it.severity}] ` : "";
2355
+ text += `- ${sev}${it.title} (id: ${it.id})\n`;
2356
+ let body = (it.body || "").trim();
2357
+ if (body.length > 300)
2358
+ body = body.slice(0, 300).trimEnd() + "…";
2359
+ if (body)
2360
+ text += ` ${body.replace(/\n+/g, " ")}\n`;
2361
+ if (it.cta_url)
2362
+ text += ` → ${it.cta_label ? `${it.cta_label}: ` : ""}${it.cta_url}\n`;
2363
+ });
2364
+ text += `\nCall notification_ack(id) once the user has seen one so it stops appearing.`;
2365
+ return { content: [{ type: "text", text }] };
2366
+ }
2367
+ catch (error) {
2368
+ if (error?.response?.status === 404) {
2369
+ // CP without the notifications endpoint yet — treat as empty, not error.
2370
+ return { content: [{ type: "text", text: "No pending Frinus notifications." }] };
2371
+ }
2372
+ const apiErr = handleApiError(error);
2373
+ if (apiErr)
2374
+ return apiErr;
2375
+ throw error;
2376
+ }
2377
+ },
2378
+ async notification_ack(args, { cpClient }) {
2379
+ const id = args.id || "";
2380
+ if (!id) {
2381
+ return {
2382
+ content: [{ type: "text", text: "notification_ack requires an 'id'." }],
2383
+ isError: true,
2384
+ };
2385
+ }
2386
+ try {
2387
+ await cpClient.ackNotification(id);
2388
+ return {
2389
+ content: [{
2390
+ type: "text",
2391
+ text: `Notification ${id} acknowledged. It will no longer appear in the inbox.`,
2392
+ }],
2393
+ };
2394
+ }
2395
+ catch (error) {
2396
+ if (error?.response?.status === 404) {
2397
+ return {
2398
+ content: [{
2399
+ type: "text",
2400
+ text: `Notification ${id} not found (already acknowledged, expired, or not for you).`,
2401
+ }],
2402
+ };
2403
+ }
2404
+ const apiErr = handleApiError(error);
2405
+ if (apiErr)
2406
+ return apiErr;
2407
+ throw error;
2408
+ }
2409
+ },
2319
2410
  };
2320
2411
  // ---------------------------------------------------------------------------
2321
2412
  // Dispatcher: looks up the handler and executes it with error handling
@@ -58,6 +58,24 @@ export interface AgentClientInterface {
58
58
  timeout_seconds?: number;
59
59
  }): Promise<any>;
60
60
  }
61
+ /** A single platform notification served by the Control Plane inbox. */
62
+ export interface NotificationItem {
63
+ id: string;
64
+ slug: string;
65
+ title: string;
66
+ body: string;
67
+ kind: string;
68
+ severity: string;
69
+ cta_label?: string | null;
70
+ cta_url?: string | null;
71
+ starts_at?: string | null;
72
+ expires_at?: string | null;
73
+ }
74
+ /** Response shape of GET /api/v1/notifications/inbox. */
75
+ export interface NotificationInbox {
76
+ items: NotificationItem[];
77
+ count: number;
78
+ }
61
79
  /** Public surface of the CpClient used by handlers. */
62
80
  export interface CpClientInterface {
63
81
  createUniverse(orgId: string, data: {
@@ -71,6 +89,12 @@ export interface CpClientInterface {
71
89
  description?: string;
72
90
  }): Promise<any>;
73
91
  deleteUniverse(orgId: string, universeId: string): Promise<void>;
92
+ getNotificationInbox(opts?: {
93
+ channel?: string;
94
+ limit?: number;
95
+ markDelivered?: boolean;
96
+ }): Promise<NotificationInbox>;
97
+ ackNotification(id: string): Promise<void>;
74
98
  storeCredential(integrationRef: string, data: Record<string, unknown>, metadata?: Record<string, unknown>): Promise<any>;
75
99
  getCredential(integrationRef: string): Promise<any>;
76
100
  listCredentials(): Promise<any>;
package/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "manifest_version": "0.3",
3
3
  "name": "frinus",
4
4
  "display_name": "Frinus — Memória Cognitiva",
5
- "version": "3.6.4",
5
+ "version": "3.7.0",
6
6
  "description": "Memória cognitiva de longo prazo para o Claude (φρήν + νοῦς). 83 ferramentas de memória, sessões, agentes e conhecimento.",
7
7
  "long_description": "O Frinus dá ao Claude memória de longo prazo, identidade e inteligência persistente entre conversas. Conecta o Claude Desktop à plataforma Frinus (Memory Engine, Control Plane e Agent Service) via MCP. Basta colar sua chave de API pessoal — nada para instalar ou compilar.",
8
8
  "author": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frinus-mcp",
3
- "version": "3.6.4",
3
+ "version": "3.7.0",
4
4
  "description": "Frinus - MCP Server for Cognitive Memory (φρήν + νοῦς). Long-term memory, sessions, agents and knowledge for Claude and any MCP client.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",