frinus-mcp 3.1.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,460 @@
1
+ /**
2
+ * HTTP client for the Frinus Memory Service.
3
+ */
4
+ import axios from "axios";
5
+ /**
6
+ * Mutable tenant / user state resolved at startup.
7
+ * Shared with the rest of the server via the exported getters/setters below.
8
+ *
9
+ * NOTE: These are module-level singletons, not instance properties, because
10
+ * they are consumed by multiple client classes (MemoryClient, AgentClient,
11
+ * CpClient) and tool handlers via Axios request interceptors that capture
12
+ * the getter at construction time. Moving them to a single class instance
13
+ * would require threading that instance through every consumer, which is
14
+ * disproportionate for an MCP server that runs as a single-process stdio
15
+ * bridge. Use `resetState()` in tests or when reinitializing the server.
16
+ */
17
+ let resolvedTenantOrgId = process.env.FRINUS_TENANT_ORG_ID || null;
18
+ let resolvedUserEmail = null;
19
+ let resolvedUserId = null;
20
+ export function getResolvedTenantOrgId() { return resolvedTenantOrgId; }
21
+ export function setResolvedTenantOrgId(v) { resolvedTenantOrgId = v; }
22
+ export function getResolvedUserEmail() { return resolvedUserEmail; }
23
+ export function setResolvedUserEmail(v) { resolvedUserEmail = v; }
24
+ export function getResolvedUserId() { return resolvedUserId; }
25
+ export function setResolvedUserId(v) { resolvedUserId = v; }
26
+ /** Reset all mutable module state. Useful for tests or server reinitialization. */
27
+ export function resetState() {
28
+ resolvedTenantOrgId = process.env.FRINUS_TENANT_ORG_ID || null;
29
+ resolvedUserEmail = null;
30
+ resolvedUserId = null;
31
+ }
32
+ // Memory service client
33
+ export class MemoryClient {
34
+ client;
35
+ constructor(baseURL, apiKey) {
36
+ this.client = axios.create({
37
+ baseURL,
38
+ timeout: 30000,
39
+ });
40
+ // Intercept every request to inject X-API-Key and X-Tenant-ID
41
+ this.client.interceptors.request.use((config) => {
42
+ config.headers = config.headers || {};
43
+ config.headers['X-API-Key'] = apiKey;
44
+ if (resolvedTenantOrgId) {
45
+ config.headers['X-Tenant-ID'] = resolvedTenantOrgId;
46
+ }
47
+ return config;
48
+ });
49
+ }
50
+ /** Return the current tenant org ID (if resolved). */
51
+ getTenantOrgId() {
52
+ return resolvedTenantOrgId;
53
+ }
54
+ async getAuthMe() {
55
+ const response = await this.client.get("/auth/me");
56
+ return response.data;
57
+ }
58
+ // =========================================================================
59
+ // Session Management
60
+ // =========================================================================
61
+ async startSession(data) {
62
+ const response = await this.client.post("/session/start", data);
63
+ return response.data;
64
+ }
65
+ async endSession(sessionId) {
66
+ const response = await this.client.post(`/session/${sessionId}/end`);
67
+ return response.data;
68
+ }
69
+ async getSessionContext(data) {
70
+ const response = await this.client.post("/session/context", data);
71
+ return response.data;
72
+ }
73
+ async getSessionSummary(sessionId) {
74
+ const response = await this.client.get(`/session/summary/${encodeURIComponent(sessionId)}`);
75
+ return response.data;
76
+ }
77
+ async clearSession(sessionId) {
78
+ const response = await this.client.delete(`/session/clear/${encodeURIComponent(sessionId)}`);
79
+ return response.data;
80
+ }
81
+ // =========================================================================
82
+ // Core Memory CRUD
83
+ // =========================================================================
84
+ async storeMemory(data) {
85
+ const payload = {
86
+ agent_id: data.agent_id,
87
+ content: data.content,
88
+ memory_type: data.memory_type || "episodic",
89
+ scope: data.scope || "organization",
90
+ importance: data.importance || 0.5,
91
+ metadata: data.metadata,
92
+ };
93
+ if (data.user_id)
94
+ payload.user_id = data.user_id;
95
+ if (data.created_by_user_id)
96
+ payload.created_by_user_id = data.created_by_user_id;
97
+ if (data.context_id)
98
+ payload.context_id = data.context_id;
99
+ if (data.credential_ref)
100
+ payload.credential_ref = data.credential_ref;
101
+ const response = await this.client.post("/memories", payload);
102
+ return response.data;
103
+ }
104
+ async searchMemories(data) {
105
+ const response = await this.client.post("/memories/search", {
106
+ query_text: data.query_text,
107
+ agent_id: data.agent_id,
108
+ memory_types: data.memory_types,
109
+ limit: data.limit || 10,
110
+ });
111
+ return response.data;
112
+ }
113
+ async getMemory(memoryId) {
114
+ const response = await this.client.get(`/memories/${memoryId}`);
115
+ return response.data;
116
+ }
117
+ async getAgentMemories(agentId, memoryType, limit = 50) {
118
+ const params = new URLSearchParams({ limit: limit.toString() });
119
+ if (memoryType)
120
+ params.append("memory_type", memoryType);
121
+ const response = await this.client.get(`/memories/agent/${agentId}?${params}`);
122
+ return response.data;
123
+ }
124
+ async deleteMemory(memoryId) {
125
+ const response = await this.client.delete(`/memories/${memoryId}`);
126
+ // API returns 204 No Content on successful delete
127
+ if (response.status === 204) {
128
+ return { deleted: true };
129
+ }
130
+ return response.data;
131
+ }
132
+ async buildContext(data) {
133
+ const response = await this.client.post("/memories/context", {
134
+ agent_id: data.agent_id,
135
+ task_description: data.task_description,
136
+ max_tokens: data.max_tokens || 2000,
137
+ });
138
+ return response.data;
139
+ }
140
+ // =========================================================================
141
+ // Dynamic Relevance
142
+ // =========================================================================
143
+ async reinforceMemory(memoryId, boost) {
144
+ const response = await this.client.post(`/memories/${memoryId}/reinforce`, { boost });
145
+ return response.data;
146
+ }
147
+ async weakenMemory(memoryId, penalty) {
148
+ const response = await this.client.post(`/memories/${memoryId}/weaken`, { penalty });
149
+ return response.data;
150
+ }
151
+ // =========================================================================
152
+ // Working Memory
153
+ // =========================================================================
154
+ async getWorkingMemory(contextId) {
155
+ const response = await this.client.get(`/working-memory/${contextId}`);
156
+ return response.data;
157
+ }
158
+ async addWorkingMemory(data) {
159
+ const response = await this.client.post("/working-memory", data);
160
+ return response.data;
161
+ }
162
+ async clearWorkingMemory(contextId) {
163
+ const response = await this.client.delete(`/working-memory/${contextId}`);
164
+ return response.data;
165
+ }
166
+ // =========================================================================
167
+ // Stream
168
+ // =========================================================================
169
+ async captureStream(data) {
170
+ const response = await this.client.post("/stream/capture", data);
171
+ return response.data;
172
+ }
173
+ async getSessionItems(sessionId, limit) {
174
+ const params = limit ? `?limit=${limit}` : "";
175
+ const response = await this.client.get(`/stream/session/${encodeURIComponent(sessionId)}${params}`);
176
+ return response.data;
177
+ }
178
+ async getRecentStreamItems(agentId, limit, includeForgotten) {
179
+ const params = new URLSearchParams();
180
+ if (agentId)
181
+ params.append("agent_id", agentId);
182
+ if (limit)
183
+ params.append("limit", limit.toString());
184
+ if (includeForgotten)
185
+ params.append("include_forgotten", "true");
186
+ const query = params.toString() ? `?${params}` : "";
187
+ const response = await this.client.get(`/stream/recent${query}`);
188
+ return response.data;
189
+ }
190
+ async processStream(batchSize) {
191
+ const params = batchSize ? `?batch_size=${batchSize}` : "";
192
+ const response = await this.client.post(`/stream/process${params}`);
193
+ return response.data;
194
+ }
195
+ // =========================================================================
196
+ // Selective Attention
197
+ // =========================================================================
198
+ async searchWithAttention(data) {
199
+ const response = await this.client.post("/memories/search-with-attention", data);
200
+ return response.data;
201
+ }
202
+ // =========================================================================
203
+ // User
204
+ // =========================================================================
205
+ async getUserContext(taskDescription, userEmail) {
206
+ const response = await this.client.post("/memories/context", {
207
+ task_description: taskDescription,
208
+ user_email: userEmail,
209
+ });
210
+ return response.data;
211
+ }
212
+ // =========================================================================
213
+ // Maintenance
214
+ // =========================================================================
215
+ async heartbeatTick(data) {
216
+ const response = await this.client.post("/heartbeat/tick", data);
217
+ return response.data;
218
+ }
219
+ async reorientAgent(agentId, focus) {
220
+ const payload = {};
221
+ if (focus)
222
+ payload.focus = focus;
223
+ const response = await this.client.post(`/agents/${agentId}/reorient`, payload);
224
+ return response.data;
225
+ }
226
+ async runSleepCycle(phases) {
227
+ const response = await this.client.post("/sleep/run", {
228
+ phases: phases || ["evaluation", "forgetting", "consolidation", "relevance"],
229
+ });
230
+ return response.data;
231
+ }
232
+ // =========================================================================
233
+ // Consolidation (Active Forgetting)
234
+ // =========================================================================
235
+ async detectConflicts(similarityThreshold, limit) {
236
+ const response = await this.client.post("/consolidation/detect-conflicts", {
237
+ similarity_threshold: similarityThreshold || 0.9,
238
+ limit: limit || 50,
239
+ });
240
+ return response.data;
241
+ }
242
+ async resolveConflict(data) {
243
+ const response = await this.client.post("/consolidation/resolve-conflict", {
244
+ keep_id: data.keep_id,
245
+ supersede_id: data.supersede_id,
246
+ resolution_note: data.resolution_note,
247
+ });
248
+ return response.data;
249
+ }
250
+ async detectRedundant(similarityThreshold, limit) {
251
+ const response = await this.client.post("/consolidation/detect-redundant", {
252
+ similarity_threshold: similarityThreshold || 0.95,
253
+ limit: limit || 50,
254
+ });
255
+ return response.data;
256
+ }
257
+ // =========================================================================
258
+ // Hierarchical Memory
259
+ // =========================================================================
260
+ async hierarchyConsolidate(data) {
261
+ const response = await this.client.post("/hierarchy/consolidate", {
262
+ memory_ids: data.memory_ids,
263
+ summary_content: data.summary_content,
264
+ agent_id: data.agent_id,
265
+ });
266
+ return response.data;
267
+ }
268
+ async getHierarchyTree(memoryId) {
269
+ const response = await this.client.get(`/hierarchy/${memoryId}/tree`);
270
+ return response.data;
271
+ }
272
+ // =========================================================================
273
+ // Knowledge Graph - Concepts
274
+ // =========================================================================
275
+ async createConcept(data) {
276
+ const response = await this.client.post("/graph/concepts", data);
277
+ return response.data;
278
+ }
279
+ async listUniverseConcepts(universeId) {
280
+ const response = await this.client.get(`/graph/universes/${universeId}/concepts`);
281
+ return response.data;
282
+ }
283
+ async updateConcept(conceptId, data) {
284
+ const response = await this.client.put(`/graph/concepts/${conceptId}`, data);
285
+ return response.data;
286
+ }
287
+ async deleteConcept(conceptId) {
288
+ const response = await this.client.delete(`/graph/concepts/${conceptId}`);
289
+ if (response.status === 204) {
290
+ return { deleted: true };
291
+ }
292
+ return response.data;
293
+ }
294
+ // =========================================================================
295
+ // Knowledge Graph - Themes
296
+ // =========================================================================
297
+ async createTheme(data) {
298
+ const response = await this.client.post("/graph/themes", data);
299
+ return response.data;
300
+ }
301
+ async listConceptThemes(conceptId) {
302
+ const response = await this.client.get(`/graph/concepts/${conceptId}/themes`);
303
+ return response.data;
304
+ }
305
+ async updateTheme(themeId, data) {
306
+ const response = await this.client.put(`/graph/themes/${themeId}`, data);
307
+ return response.data;
308
+ }
309
+ async deleteTheme(themeId) {
310
+ const response = await this.client.delete(`/graph/themes/${themeId}`);
311
+ if (response.status === 204) {
312
+ return { deleted: true };
313
+ }
314
+ return response.data;
315
+ }
316
+ // =========================================================================
317
+ // Knowledge Graph - Topics
318
+ // =========================================================================
319
+ async createTopic(data) {
320
+ const response = await this.client.post("/graph/topics", data);
321
+ return response.data;
322
+ }
323
+ async listThemeTopics(themeId) {
324
+ const response = await this.client.get(`/graph/themes/${themeId}/topics`);
325
+ return response.data;
326
+ }
327
+ async updateTopic(topicId, data) {
328
+ const response = await this.client.put(`/graph/topics/${topicId}`, data);
329
+ return response.data;
330
+ }
331
+ async updateTopicStatus(topicId, status) {
332
+ const response = await this.client.patch(`/graph/topics/${topicId}/status`, { status });
333
+ return response.data;
334
+ }
335
+ async deleteTopic(topicId) {
336
+ const response = await this.client.delete(`/graph/topics/${topicId}`);
337
+ if (response.status === 204) {
338
+ return { deleted: true };
339
+ }
340
+ return response.data;
341
+ }
342
+ // =========================================================================
343
+ // Knowledge Graph - Points
344
+ // =========================================================================
345
+ async createPoint(data) {
346
+ const response = await this.client.post("/graph/points", data);
347
+ return response.data;
348
+ }
349
+ async listTopicPoints(topicId) {
350
+ const response = await this.client.get(`/graph/topics/${topicId}/points`);
351
+ return response.data;
352
+ }
353
+ async updatePoint(pointId, data) {
354
+ const response = await this.client.put(`/graph/points/${pointId}`, data);
355
+ return response.data;
356
+ }
357
+ async updatePointStatus(pointId, status) {
358
+ const response = await this.client.patch(`/graph/points/${pointId}/status`, { status });
359
+ return response.data;
360
+ }
361
+ async deletePoint(pointId) {
362
+ const response = await this.client.delete(`/graph/points/${pointId}`);
363
+ if (response.status === 204) {
364
+ return { deleted: true };
365
+ }
366
+ return response.data;
367
+ }
368
+ // =========================================================================
369
+ // Knowledge Graph - Hierarchy
370
+ // =========================================================================
371
+ async getUniverseHierarchy(universeId) {
372
+ const response = await this.client.get(`/graph/universes/${universeId}/hierarchy`);
373
+ return response.data;
374
+ }
375
+ // =========================================================================
376
+ // Training
377
+ // =========================================================================
378
+ async trainingTeach(data) {
379
+ const response = await this.client.post("/training/teach", {
380
+ content: data.content,
381
+ type: data.type || "semantic",
382
+ importance: data.importance || 0.7,
383
+ universe_id: data.universe_id,
384
+ });
385
+ return response.data;
386
+ }
387
+ async trainingQa(data) {
388
+ const response = await this.client.post("/training/qa", {
389
+ pairs: data.pairs,
390
+ universe_id: data.universe_id,
391
+ });
392
+ return response.data;
393
+ }
394
+ async trainingStats() {
395
+ const response = await this.client.get("/training/stats");
396
+ return response.data;
397
+ }
398
+ async trainingGaps() {
399
+ const response = await this.client.get("/training/gaps");
400
+ return response.data;
401
+ }
402
+ async trainingRecent(data) {
403
+ const params = data?.limit ? `?limit=${data.limit}` : "";
404
+ const response = await this.client.get(`/training/recent${params}`);
405
+ return response.data;
406
+ }
407
+ // -------------------------------------------------------------------------
408
+ // Task Management
409
+ // -------------------------------------------------------------------------
410
+ async createTask(data) {
411
+ const response = await this.client.post("/tasks", data);
412
+ return response.data;
413
+ }
414
+ async getTask(taskId) {
415
+ const response = await this.client.get(`/tasks/${taskId}`);
416
+ return response.data;
417
+ }
418
+ async listTasks(data) {
419
+ const params = new URLSearchParams();
420
+ if (data?.status)
421
+ params.append("status", data.status);
422
+ if (data?.assigned_agent_id)
423
+ params.append("assigned_agent_id", data.assigned_agent_id);
424
+ if (data?.parent_task_id)
425
+ params.append("parent_task_id", data.parent_task_id);
426
+ if (data?.limit)
427
+ params.append("limit", data.limit.toString());
428
+ const qs = params.toString();
429
+ const response = await this.client.get(`/tasks${qs ? `?${qs}` : ""}`);
430
+ return response.data;
431
+ }
432
+ async updateTask(taskId, data) {
433
+ const response = await this.client.patch(`/tasks/${taskId}`, data);
434
+ return response.data;
435
+ }
436
+ // =========================================================================
437
+ // Skills
438
+ // =========================================================================
439
+ async listSkills(category) {
440
+ const params = {};
441
+ if (category)
442
+ params.category = category;
443
+ const response = await this.client.get('/skills', { params });
444
+ return response.data;
445
+ }
446
+ async getAgentSkills(agentId) {
447
+ const response = await this.client.get(`/skills/agent/${agentId}`);
448
+ return response.data;
449
+ }
450
+ async assignSkill(agentId, skillId, permission) {
451
+ const response = await this.client.post(`/skills/agent/${agentId}/${skillId}`, {
452
+ permission: permission || 'execute',
453
+ });
454
+ return response.data;
455
+ }
456
+ async removeSkill(agentId, skillId) {
457
+ const response = await this.client.delete(`/skills/agent/${agentId}/${skillId}`);
458
+ return response.data;
459
+ }
460
+ }
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP Server for Agents Memory Service
4
+ *
5
+ * This server exposes the memory service as MCP tools for Claude agents.
6
+ *
7
+ * Module structure:
8
+ * client/memory-client.ts - HTTP client (MemoryClient + Axios interceptors)
9
+ * capture/interaction-capture.ts - Automatic tool-call stream tracking
10
+ * tools/definitions.ts - Tool schemas (inputSchema for all tools)
11
+ * tools/handlers.ts - Tool handlers (dispatch map)
12
+ * types/index.ts - Shared interfaces and types
13
+ */
14
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP Server for Agents Memory Service
4
+ *
5
+ * This server exposes the memory service as MCP tools for Claude agents.
6
+ *
7
+ * Module structure:
8
+ * client/memory-client.ts - HTTP client (MemoryClient + Axios interceptors)
9
+ * capture/interaction-capture.ts - Automatic tool-call stream tracking
10
+ * tools/definitions.ts - Tool schemas (inputSchema for all tools)
11
+ * tools/handlers.ts - Tool handlers (dispatch map)
12
+ * types/index.ts - Shared interfaces and types
13
+ */
14
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
15
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
17
+ import { MemoryClient, getResolvedUserEmail, getResolvedUserId, setResolvedTenantOrgId, setResolvedUserEmail, setResolvedUserId, getResolvedTenantOrgId, } from "./client/memory-client.js";
18
+ import { AgentClient } from "./client/agent-client.js";
19
+ import { CpClient } from "./client/cp-client.js";
20
+ import { InteractionCapture } from "./capture/interaction-capture.js";
21
+ import { TOOLS } from "./tools/definitions.js";
22
+ import { dispatchTool } from "./tools/handlers.js";
23
+ // Defaults point at the Frinus SaaS so `npx -y frinus-mcp` works with only an
24
+ // API key. Local development overrides these via env (MEMORY_SERVICE_URL, etc.).
25
+ const MEMORY_SERVICE_URL = process.env.MEMORY_SERVICE_URL || "https://frinus-memory.rdxsec.com.br";
26
+ const CP_URL = process.env.FRINUS_CP_URL || "https://frinus-api.rdxsec.com.br";
27
+ const AGENT_SERVICE_URL = process.env.AGENT_SERVICE_URL || "https://frinus-agents.rdxsec.com.br";
28
+ // Main server
29
+ async function main() {
30
+ // Require API key (FRINUS_API_KEY preferred, FRINUS_MEMORY_API_KEY for backward compat)
31
+ const apiKey = process.env.FRINUS_API_KEY || process.env.FRINUS_MEMORY_API_KEY;
32
+ if (!apiKey) {
33
+ console.error("[FATAL] FRINUS_API_KEY environment variable is required but not set.");
34
+ console.error("Generate an API key in the Frinus dashboard or via the Control Plane API.");
35
+ process.exit(1);
36
+ }
37
+ const memoryClient = new MemoryClient(MEMORY_SERVICE_URL, apiKey);
38
+ const agentClient = new AgentClient(AGENT_SERVICE_URL, apiKey);
39
+ const cpClient = new CpClient(CP_URL, apiKey);
40
+ const capture = new InteractionCapture(memoryClient);
41
+ console.error(`[InteractionCapture] Session: ${capture.getSessionId()}`);
42
+ // Validate API key and resolve tenant identity at startup
43
+ try {
44
+ const me = await memoryClient.getAuthMe();
45
+ setResolvedUserEmail(me.email || me.user?.email || null);
46
+ setResolvedUserId(me.id || me.user?.id || me.user_id || null);
47
+ // Resolve tenant org ID from /auth/me response (env var takes precedence)
48
+ const meOrgId = me.organization_id || me.org_id || me.user?.organization_id || null;
49
+ if (!getResolvedTenantOrgId() && meOrgId) {
50
+ setResolvedTenantOrgId(meOrgId);
51
+ }
52
+ console.error(`[Auth] API key validated. User: ${getResolvedUserEmail()}, Tenant: ${getResolvedTenantOrgId() || 'none'}`);
53
+ }
54
+ catch (err) {
55
+ const status = err?.response?.status;
56
+ if (status === 401 || status === 403) {
57
+ console.error(`[FATAL] API key rejected (HTTP ${status}). Check FRINUS_API_KEY.`);
58
+ }
59
+ else {
60
+ console.error(`[FATAL] Failed to validate API key: ${err?.message || err}`);
61
+ }
62
+ process.exit(1);
63
+ }
64
+ const server = new Server({
65
+ name: "frinus",
66
+ version: "3.1.0",
67
+ }, {
68
+ capabilities: {
69
+ tools: {},
70
+ },
71
+ });
72
+ // List tools handler (all tools always visible; backend enforces auth)
73
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
74
+ tools: TOOLS,
75
+ }));
76
+ // Call tool handler (with automatic interaction capture)
77
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
78
+ const { name, arguments: args = {} } = request.params;
79
+ // Execute the tool via the dispatch map
80
+ const toolResult = await dispatchTool(name, args, {
81
+ memoryClient,
82
+ agentClient,
83
+ cpClient,
84
+ capture,
85
+ resolvedUserEmail: getResolvedUserEmail(),
86
+ resolvedUserId: getResolvedUserId(),
87
+ cpUrl: CP_URL,
88
+ });
89
+ // Fire-and-forget: capture the tool call interaction to the memory stream
90
+ try {
91
+ capture.captureToolCall(name, args, toolResult);
92
+ }
93
+ catch (err) {
94
+ console.error('[MCP] Capture failed:', err.message);
95
+ }
96
+ return toolResult;
97
+ });
98
+ // Start server
99
+ const transport = new StdioServerTransport();
100
+ await server.connect(transport);
101
+ console.error("MCP Memory Server running on stdio");
102
+ }
103
+ main().catch(console.error);
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Tool schema definitions for the Frinus MCP Server.
3
+ *
4
+ * Each entry defines a tool name, description, and inputSchema
5
+ * following the MCP Tool specification.
6
+ */
7
+ import { Tool } from "@modelcontextprotocol/sdk/types.js";
8
+ export declare const TOOLS: Tool[];