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.
- package/dist/client/cp-client.d.ts +7 -1
- package/dist/client/cp-client.js +26 -0
- package/dist/index.js +50 -19
- package/dist/notifications.d.ts +30 -0
- package/dist/notifications.js +86 -0
- package/dist/tools/definitions.js +39 -0
- package/dist/tools/handlers.js +93 -2
- package/dist/types/index.d.ts +24 -0
- package/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -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>;
|
package/dist/client/cp-client.js
CHANGED
|
@@ -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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
];
|
package/dist/tools/handlers.js
CHANGED
|
@@ -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
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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",
|