frinus-mcp 3.6.4 → 3.8.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.
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Boot-contract state for the MCP process (one instance per stdio session).
3
+ *
4
+ * WHY THIS EXISTS
5
+ * ---------------
6
+ * The thin-client boot protocol used to depend entirely on the model
7
+ * remembering to call `agent_reorient` "every N answers". Models forget. This
8
+ * module makes the reorient cadence DETERMINISTIC and server-side (here, inside
9
+ * the MCP process) instead of a hope:
10
+ *
11
+ * - `agent_bootstrap` calls `markBooted()` with the agent's reorient config
12
+ * (read from the authoritative Control Plane row via the bootstrap payload).
13
+ * - Every subsequent tool call increments a counter (`noteToolCall`).
14
+ * - Once the counter reaches `reorientEveryN`, `reorientBanner()` returns a
15
+ * "REORIENT DUE" notice that `index.ts` appends to EVERY tool result until
16
+ * `agent_reorient` is actually called, which resets the counter.
17
+ *
18
+ * The context-percentage trigger (`reorientOnContextPct`) cannot be enforced
19
+ * here — the MCP has no visibility into the model's token window — so it stays a
20
+ * model-side heuristic documented in the Boot Contract and the server
21
+ * `instructions`. The N-count is the half we CAN guarantee, and we guarantee it.
22
+ *
23
+ * State is per-process (= per session). Nothing here is persisted.
24
+ */
25
+ const DEFAULT_CONFIG = {
26
+ reorientEveryN: 10,
27
+ reorientOnContextPct: 70,
28
+ };
29
+ let config = { ...DEFAULT_CONFIG };
30
+ let booted = false;
31
+ let bootAgentId = null;
32
+ let sessionId = null;
33
+ let toolCallsSinceReorient = 0;
34
+ /**
35
+ * Tools that must never count toward the cadence: `agent_bootstrap` seeds it and
36
+ * `agent_reorient` IS the reorientation (and resets the counter).
37
+ */
38
+ const EXEMPT_TOOLS = new Set(["agent_bootstrap", "agent_reorient"]);
39
+ /** Mark the process as booted and (re)seed the reorient cadence + session id. */
40
+ export function markBooted(opts) {
41
+ booted = true;
42
+ bootAgentId = opts.agentId;
43
+ if (opts.sessionId)
44
+ sessionId = opts.sessionId;
45
+ if (opts.config) {
46
+ const n = opts.config.reorientEveryN;
47
+ if (typeof n === "number" && Number.isFinite(n) && n >= 1) {
48
+ config.reorientEveryN = Math.floor(n);
49
+ }
50
+ const pct = opts.config.reorientOnContextPct;
51
+ if (typeof pct === "number" && Number.isFinite(pct) && pct >= 1 && pct <= 100) {
52
+ config.reorientOnContextPct = Math.floor(pct);
53
+ }
54
+ }
55
+ toolCallsSinceReorient = 0;
56
+ }
57
+ /** Reset the cadence counter (called when the agent actually reorients). */
58
+ export function resetReorientCounter() {
59
+ toolCallsSinceReorient = 0;
60
+ }
61
+ /**
62
+ * Record a tool call. Exempt tools do not increment; `agent_reorient` resets.
63
+ * No-op until the process has booted, so pre-boot probing never arms the banner.
64
+ */
65
+ export function noteToolCall(name) {
66
+ if (!booted)
67
+ return;
68
+ if (EXEMPT_TOOLS.has(name)) {
69
+ if (name === "agent_reorient")
70
+ resetReorientCounter();
71
+ return;
72
+ }
73
+ toolCallsSinceReorient += 1;
74
+ }
75
+ /** True once at least `reorientEveryN` counted tool calls have elapsed. */
76
+ export function reorientDue() {
77
+ return booted && toolCallsSinceReorient >= config.reorientEveryN;
78
+ }
79
+ /**
80
+ * The "REORIENT DUE" banner, or null when not due. Deliberately imperative and
81
+ * self-explaining so it survives even if the model ignored the Boot Contract.
82
+ */
83
+ export function reorientBanner() {
84
+ if (!reorientDue())
85
+ return null;
86
+ const id = bootAgentId ?? "<agent_id>";
87
+ return (`\n\n---\n` +
88
+ `⚠️ REORIENT DEVIDO: ${toolCallsSinceReorient} tool calls desde o último ` +
89
+ `boot/reorient (limite ${config.reorientEveryN}). Chame ` +
90
+ `\`agent_reorient(agent_id="${id}")\` agora para se re-fundamentar antes de ` +
91
+ `continuar. Este aviso repete a cada tool call até você reorientar.`);
92
+ }
93
+ export function getBootAgentId() {
94
+ return bootAgentId;
95
+ }
96
+ export function getSessionId() {
97
+ return sessionId;
98
+ }
99
+ export function setSessionId(id) {
100
+ sessionId = id;
101
+ }
102
+ export function isBooted() {
103
+ return booted;
104
+ }
105
+ export function getReorientConfig() {
106
+ return { ...config };
107
+ }
108
+ /** Test-only: reset all module state. Not part of the runtime surface. */
109
+ export function __resetBootState() {
110
+ config = { ...DEFAULT_CONFIG };
111
+ booted = false;
112
+ bootAgentId = null;
113
+ sessionId = null;
114
+ toolCallsSinceReorient = 0;
115
+ }
@@ -9,6 +9,19 @@ export const TOOLS = [
9
9
  Use this to record important information, learnings, decisions, or procedures
10
10
  that should be remembered for future reference.
11
11
 
12
+ ANCHORING (do this first): memory lives in a tree — Universe > Concept > Theme >
13
+ Topic > Point. Before storing, call hierarchy_find("<subject>") and copy the best
14
+ candidate's context_path (BY NAME, e.g. "Plataforma Frinus > Memory Engine") into
15
+ context_path below. The path raises the classifier's confidence and anchors the
16
+ memory on the right node. Never invent a UUID — you get names right, UUIDs wrong.
17
+
18
+ PRECEDENCE of the placement arguments (highest first):
19
+ context_id (UUID) > context_path (name) > universe_id > the agent's own
20
+ universe > auto-classification > the org's "Geral" (floor, never NULL).
21
+ A context_path that is ambiguous or whose segments don't all match never creates
22
+ a node and never fails the write: it anchors to the deepest segment that matched
23
+ and the response carries a non-fatal context_path_warning saying so.
24
+
12
25
  The backend classifies the memory by content at write time and proposes which
13
26
  universe/context node it belongs to. When the tenant has auto-classification
14
27
  enabled and the proposal's confidence meets the tenant threshold (the platform
@@ -17,11 +30,11 @@ is moved without review. Below the threshold (or with auto-classification off)
17
30
  the proposal stays a PENDING suggestion: review it with
18
31
  memory_classification_pending and apply with memory_classification_approve.
19
32
 
20
- Passing universe_id or context_id anchors the memory THERE directly (status
21
- "manual"): auto-classification is skipped entirely, so no suggestion is returned
22
- and nothing may later move it. The response always reports the destination the
23
- memory actually landed in — a suggestion that is still pending is labelled as
24
- such and has NOT been applied.
33
+ Passing context_path, universe_id or context_id anchors the memory accordingly
34
+ (status "manual" for UUIDs): auto-classification is skipped and nothing may later
35
+ move it. The response always reports the destination the memory actually landed
36
+ in — a suggestion that is still pending is labelled as such and has NOT been
37
+ applied.
25
38
 
26
39
  Memory types:
27
40
  - episodic: Specific experiences and events (what happened)
@@ -88,7 +101,11 @@ Scopes:
88
101
  },
89
102
  context_id: {
90
103
  type: "string",
91
- description: "Optional UUID of a hierarchy node (universe, concept, theme, topic, or point). When provided, the memory is automatically linked to this node and all its ancestors. Takes precedence over universe_id for hierarchy placement.",
104
+ description: "Optional UUID of a hierarchy node (universe, concept, theme, topic, or point). When provided, the memory is automatically linked to this node and all its ancestors. Highest-precedence placement argument (above context_path and universe_id).",
105
+ },
106
+ context_path: {
107
+ type: "string",
108
+ description: "Anchor the memory BY NAME with a breadcrumb like 'Universe > Concept > Theme' (separators /, > or › all work). Call hierarchy_find first and copy the best candidate's context_path here — this is the preferred way to place a memory. The backend resolves names to a node deterministically (never creates nodes). Takes precedence over universe_id, but not over context_id. If a segment is ambiguous or unknown the write still succeeds, anchors to the deepest matched segment, and returns a context_path_warning.",
92
109
  },
93
110
  credential_ref: {
94
111
  type: "string",
@@ -98,6 +115,43 @@ Scopes:
98
115
  required: ["agent_id", "content"],
99
116
  },
100
117
  },
118
+ {
119
+ name: "hierarchy_find",
120
+ description: `Find where something belongs in the memory tree (Universe >
121
+ Concept > Theme > Topic > Point).
122
+
123
+ Read-only: it never writes a memory or a node. It runs the same classifier the
124
+ backend uses at write time and returns the top candidate nodes for your query,
125
+ each with its full path BY NAME and a confidence in [0,1].
126
+
127
+ Call it in two moments:
128
+ - BEFORE memory_store — copy the best candidate's context_path (by name) into
129
+ memory_store(context_path=...) so the memory anchors on the right node and
130
+ clears the 0.60 auto-anchor threshold instead of landing in the review queue.
131
+ - BEFORE a tree-specific search — to confirm which branch to look in.
132
+
133
+ Copy the context_path (the names), never the context_id (UUID). Universe
134
+ permission is enforced: a restricted caller only sees candidates in universes it
135
+ may read, and an explicit universe_id it cannot read is rejected (403).`,
136
+ inputSchema: {
137
+ type: "object",
138
+ properties: {
139
+ query_text: {
140
+ type: "string",
141
+ description: "What to situate in the tree — the subject of the memory you are about to store, or the search you are about to run.",
142
+ },
143
+ universe_id: {
144
+ type: "string",
145
+ description: "Optional UUID to restrict candidates to one universe (must be one you may read). Omit to search every universe you can access.",
146
+ },
147
+ limit: {
148
+ type: "integer",
149
+ description: "Max candidates to return (default 5, max 25).",
150
+ },
151
+ },
152
+ required: ["query_text"],
153
+ },
154
+ },
101
155
  {
102
156
  name: "memory_search",
103
157
  description: `Search memories by semantic similarity.
@@ -860,7 +914,11 @@ Memories created by the agent are NOT deleted.`,
860
914
  name: "agent_bootstrap",
861
915
  description: `Bootstrap completo do agente — chame no início da sessão para carregar identidade, system prompt e contexto runtime. O agente thin-client chama isto como regra única de boot.
862
916
 
863
- Retorna identity, system_prompt, runtime (working_memory, recent_memories, skills, delegate_digest) e persona_version em uma única chamada.
917
+ A resposta começa por um "Contrato de Boot" no TOPO com o que não pode se perder: identidade (id), organização (grave memórias só nela), usuário, versão do MCP, session_id, persona_version e a cadência de reorient. O system prompt longo vem depois.
918
+
919
+ Efeitos colaterais: inicia a sessão de memória (session_id devolvido no contrato; o MCP a encerra no fim do processo) e arma a cadência de reorient — a partir daqui o MCP conta as tool calls e anexa um aviso "REORIENT DEVIDO" quando vencer.
920
+
921
+ Retorna identity, system_prompt, runtime (working_memory, recent_memories, skills, delegate_digest), config (reorient_every_n, reorient_on_context_pct, max_memories) e persona_version.
864
922
 
865
923
  O roster de especialistas NÃO vem no bootstrap — apenas um resumo (quantos e em quais universos). Use \`agent_list\` no momento da delegação para obter nomes e IDs.`,
866
924
  inputSchema: {
@@ -1855,4 +1913,107 @@ granted it and when. No secret data.`,
1855
1913
  required: ["ref"],
1856
1914
  },
1857
1915
  },
1916
+ // ==========================================================================
1917
+ // Platform Notifications (3)
1918
+ // ==========================================================================
1919
+ {
1920
+ name: "notifications_list",
1921
+ description: `List pending Frinus notifications for the current user.
1922
+
1923
+ Two scopes appear here: (Sistema) = platform-authored (new features, maintenance,
1924
+ tips, alerts), and (Organização) = authored by an owner of the user's own org.
1925
+ This is a READ-ONLY peek — it does not mark anything as delivered or acknowledged,
1926
+ so items stay in the inbox until the user sees them.
1927
+
1928
+ Each item includes an \`id\` you can pass to notification_ack once the user has
1929
+ read it. Use this when the user asks "what's new" or to re-check news that was
1930
+ surfaced earlier in the session.`,
1931
+ inputSchema: {
1932
+ type: "object",
1933
+ properties: {},
1934
+ required: [],
1935
+ },
1936
+ },
1937
+ {
1938
+ name: "notification_ack",
1939
+ description: `Acknowledge a Frinus platform notification by id.
1940
+
1941
+ Call this once the user confirms they have seen the news, or asks not to see it
1942
+ again. The notification then disappears from the inbox and will not be shown
1943
+ again (in this or future sessions). The id comes from the "📣 Novidades do
1944
+ Frinus" block or from notifications_list.`,
1945
+ inputSchema: {
1946
+ type: "object",
1947
+ properties: {
1948
+ id: {
1949
+ type: "string",
1950
+ description: "UUID of the notification to acknowledge (required)",
1951
+ },
1952
+ },
1953
+ required: ["id"],
1954
+ },
1955
+ },
1956
+ {
1957
+ name: "notification_create",
1958
+ description: `Publish an ORGANIZATION notification to the members of your org (OWNER only).
1959
+
1960
+ Use this when an org owner wants to broadcast something to their team through the
1961
+ Frinus channel: a process change, a reminder, a heads-up. The notice is delivered
1962
+ on each member's next agent_bootstrap / heartbeat and leaves their inbox when they
1963
+ acknowledge it (or after max_deliveries days of being shown-and-ignored).
1964
+
1965
+ Scope is forced server-side: the notice is bound to YOUR org and cannot target
1966
+ other orgs (that is a platform_admin "system" notice, created elsewhere). Optional
1967
+ targeting narrows WITHIN your org by role and/or specific users. Requires owner
1968
+ role — a 403 means the account is not an owner. Use severity "critical" only for
1969
+ things the member must see immediately (it is relayed before other work).`,
1970
+ inputSchema: {
1971
+ type: "object",
1972
+ properties: {
1973
+ title: { type: "string", description: "Short headline (required, 1–200 chars)" },
1974
+ body: {
1975
+ type: "string",
1976
+ description: "Message body, markdown allowed (optional, up to 2000 chars)",
1977
+ },
1978
+ kind: {
1979
+ type: "string",
1980
+ enum: ["feature", "announcement", "maintenance", "tip", "alert"],
1981
+ description: "Category (default 'announcement')",
1982
+ },
1983
+ severity: {
1984
+ type: "string",
1985
+ enum: ["info", "success", "warning", "critical"],
1986
+ description: "Visual/priority level (default 'info'; 'critical' is relayed immediately)",
1987
+ },
1988
+ target_roles: {
1989
+ type: "array",
1990
+ items: { type: "string" },
1991
+ description: "Optional: only members with one of these roles (e.g. ['trainer']). Empty = all members.",
1992
+ },
1993
+ target_user_keys: {
1994
+ type: "array",
1995
+ items: { type: "string" },
1996
+ description: "Optional: only these specific users (user id/sub or email). Empty = all members.",
1997
+ },
1998
+ cta_label: { type: "string", description: "Optional call-to-action label" },
1999
+ cta_url: {
2000
+ type: "string",
2001
+ description: "Optional call-to-action URL (must be https://)",
2002
+ },
2003
+ starts_at: {
2004
+ type: "string",
2005
+ description: "Optional ISO-8601 start time (default: now)",
2006
+ },
2007
+ expires_at: {
2008
+ type: "string",
2009
+ description: "Optional ISO-8601 expiry time (default: never)",
2010
+ },
2011
+ max_deliveries: {
2012
+ type: "number",
2013
+ description: "How many days an unseen notice is shown before it drops off (default 3, 1–100)",
2014
+ },
2015
+ },
2016
+ required: ["title"],
2017
+ },
2018
+ },
1858
2019
  ];