@olivaresai/alma-mcp 1.3.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.
Files changed (4) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +145 -0
  3. package/dist/index.js +1548 -0
  4. package/package.json +39 -0
package/dist/index.js ADDED
@@ -0,0 +1,1548 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+
7
+ // src/config.ts
8
+ function loadConfig() {
9
+ const baseUrl = process.env.ALMA_API_URL ?? process.env.ALMA_BASE_URL ?? "https://alma.olivares.ai/api/v1";
10
+ const apiKey = process.env.ALMA_API_KEY ?? "";
11
+ if (!apiKey) {
12
+ console.error("[alma-mcp] ALMA_API_KEY environment variable is required.");
13
+ process.exit(1);
14
+ }
15
+ const environmentId = process.env.ALMA_ENVIRONMENT_ID || void 0;
16
+ const cleanUrl = baseUrl.replace(/\/+$/, "");
17
+ if (!cleanUrl.startsWith("https://") && !cleanUrl.startsWith("http://localhost") && !cleanUrl.startsWith("http://127.") && !cleanUrl.startsWith("http://[::1]")) {
18
+ console.error("[alma-mcp] ALMA_API_URL must use HTTPS for non-localhost connections. Got:", cleanUrl);
19
+ process.exit(1);
20
+ }
21
+ return { baseUrl: cleanUrl, apiKey, environmentId };
22
+ }
23
+
24
+ // src/client.ts
25
+ var AlmaClient = class _AlmaClient {
26
+ constructor(config2) {
27
+ this.config = config2;
28
+ }
29
+ static DEFAULT_TIMEOUT_MS = 3e4;
30
+ static STREAM_TIMEOUT_MS = 6e4;
31
+ static MAX_RETRIES = 2;
32
+ static RETRYABLE_STATUS = /* @__PURE__ */ new Set([429, 502, 503, 504]);
33
+ headers() {
34
+ return {
35
+ "Content-Type": "application/json",
36
+ "X-API-Key": this.config.apiKey
37
+ };
38
+ }
39
+ /**
40
+ * Fetch with timeout and retry (exponential backoff).
41
+ * Retries on network errors and 502/503/504. Never retries 4xx.
42
+ */
43
+ async fetchWithRetry(url, init, timeoutMs = _AlmaClient.DEFAULT_TIMEOUT_MS) {
44
+ let lastError;
45
+ for (let attempt = 0; attempt <= _AlmaClient.MAX_RETRIES; attempt++) {
46
+ if (attempt > 0) {
47
+ const delayMs = 1e3 * 2 ** (attempt - 1);
48
+ await new Promise((r) => setTimeout(r, delayMs));
49
+ }
50
+ const controller = new AbortController();
51
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
52
+ try {
53
+ const res = await fetch(url, { ...init, signal: controller.signal });
54
+ if (res.status === 429 && attempt < _AlmaClient.MAX_RETRIES) {
55
+ const retryAfter = parseInt(res.headers.get("Retry-After") || "5", 10);
56
+ const delayMs = Math.min(retryAfter * 1e3, 3e4);
57
+ await new Promise((r) => setTimeout(r, delayMs));
58
+ lastError = new Error(`HTTP 429 Too Many Requests`);
59
+ continue;
60
+ }
61
+ if (res.status >= 400 && res.status < 500) return res;
62
+ if (_AlmaClient.RETRYABLE_STATUS.has(res.status) && attempt < _AlmaClient.MAX_RETRIES) {
63
+ lastError = new Error(`HTTP ${res.status}`);
64
+ continue;
65
+ }
66
+ return res;
67
+ } catch (err) {
68
+ lastError = err;
69
+ if (attempt >= _AlmaClient.MAX_RETRIES) break;
70
+ } finally {
71
+ clearTimeout(timer);
72
+ }
73
+ }
74
+ throw lastError;
75
+ }
76
+ async request(method, path, body) {
77
+ const url = `${this.config.baseUrl}${path}`;
78
+ const res = await this.fetchWithRetry(url, {
79
+ method,
80
+ headers: this.headers(),
81
+ body: body ? JSON.stringify(body) : void 0
82
+ });
83
+ if (!res.ok) {
84
+ const text = await res.text().catch(() => "");
85
+ let detail = text;
86
+ try {
87
+ const json = JSON.parse(text);
88
+ detail = json.error?.message ?? json.message ?? text;
89
+ } catch {
90
+ }
91
+ throw new Error(`Alma API ${method} ${path} failed (${res.status}): ${detail}`);
92
+ }
93
+ return res.json();
94
+ }
95
+ // ─── Context ─────────────────────────────────────────
96
+ async assembleContext(params) {
97
+ return this.request("POST", "/context/assemble", {
98
+ user_message: params.message,
99
+ environment_id: params.environment_id ?? this.config.environmentId,
100
+ token_budget: params.token_budget
101
+ });
102
+ }
103
+ async continueSession(params) {
104
+ return this.request("POST", "/context/continue", {
105
+ ...params,
106
+ environment_id: params.environment_id ?? this.config.environmentId
107
+ });
108
+ }
109
+ async previewContext(message) {
110
+ return this.request("POST", "/context/preview", {
111
+ message
112
+ });
113
+ }
114
+ // ─── Memories ────────────────────────────────────────
115
+ async createMemory(params) {
116
+ return this.request("POST", "/memories", {
117
+ content: params.content,
118
+ category: params.category,
119
+ importance: params.importance,
120
+ source: params.source,
121
+ environment_id: params.environment_id ?? this.config.environmentId
122
+ });
123
+ }
124
+ async searchMemories(params) {
125
+ const qs = new URLSearchParams({ q: params.q });
126
+ if (params.limit) qs.set("limit", String(params.limit));
127
+ const envId = params.environment_id ?? this.config.environmentId;
128
+ if (envId) qs.set("environment_id", envId);
129
+ return this.request(
130
+ "GET",
131
+ `/memories/search?${qs}`
132
+ );
133
+ }
134
+ async listMemories(params) {
135
+ const qs = new URLSearchParams();
136
+ if (params?.limit != null) qs.set("limit", String(params.limit));
137
+ if (params?.offset != null) qs.set("offset", String(params.offset));
138
+ if (params?.category) qs.set("category", params.category);
139
+ const envId = params?.environment_id ?? this.config.environmentId;
140
+ if (envId) qs.set("environment_id", envId);
141
+ return this.request("GET", `/memories?${qs}`);
142
+ }
143
+ // ─── Extract ─────────────────────────────────────────
144
+ async extractMemories(params) {
145
+ const raw = await this.request("POST", "/memories/extract", {
146
+ conversation: params.text,
147
+ auto_save: params.save ?? true,
148
+ environment_id: params.environment_id ?? this.config.environmentId
149
+ });
150
+ return {
151
+ memories: raw.extracted.memories,
152
+ episode: raw.extracted.episode,
153
+ saved: raw.saved
154
+ };
155
+ }
156
+ // ─── Chat ────────────────────────────────────────────
157
+ async getBudget() {
158
+ return this.request("GET", "/chat/budget");
159
+ }
160
+ async listConversations(params) {
161
+ const qs = new URLSearchParams();
162
+ const envId = params?.environment_id ?? this.config.environmentId;
163
+ if (envId) qs.set("environment_id", envId);
164
+ if (params?.limit) qs.set("limit", String(params.limit));
165
+ return this.request("GET", `/chat/conversations?${qs}`);
166
+ }
167
+ async createConversation(params) {
168
+ return this.request("POST", "/chat/conversations", {
169
+ title: params?.title,
170
+ environment_id: params?.environment_id ?? this.config.environmentId
171
+ });
172
+ }
173
+ async sendMessage(conversationId, message, model) {
174
+ const url = `${this.config.baseUrl}/chat/conversations/${conversationId}/messages`;
175
+ const body = { message };
176
+ if (model) body.model = model;
177
+ const res = await this.fetchWithRetry(
178
+ url,
179
+ {
180
+ method: "POST",
181
+ headers: this.headers(),
182
+ body: JSON.stringify(body)
183
+ },
184
+ _AlmaClient.STREAM_TIMEOUT_MS
185
+ );
186
+ if (!res.ok) {
187
+ const text2 = await res.text().catch(() => "");
188
+ throw new Error(`Chat send failed (${res.status}): ${text2}`);
189
+ }
190
+ const text = await res.text();
191
+ const rawEvents = text.split("\n\n");
192
+ let fullContent = "";
193
+ for (const rawEvent of rawEvents) {
194
+ const dataLine = rawEvent.split("\n").find((l) => l.startsWith("data: "));
195
+ if (!dataLine) continue;
196
+ const data = dataLine.slice(6).trim();
197
+ if (!data) continue;
198
+ if (data === "[DONE]") break;
199
+ try {
200
+ const event = JSON.parse(data);
201
+ if (event.type === "chunk" && event.text) {
202
+ fullContent += event.text;
203
+ } else if (event.type === "done") {
204
+ break;
205
+ } else if (event.type === "error") {
206
+ throw new Error(event.error ?? "Chat stream error");
207
+ }
208
+ } catch (e) {
209
+ if (e instanceof SyntaxError) continue;
210
+ throw e;
211
+ }
212
+ }
213
+ return fullContent;
214
+ }
215
+ // ─── Soul ────────────────────────────────────────────
216
+ // The legacy GET /soul endpoint was removed. Soul data lives in /blocks.
217
+ // We fetch blocks and transform core blocks into a soul-like key/value map.
218
+ async getSoul() {
219
+ const result = await this.listBlocks();
220
+ const soul = {};
221
+ for (const block of result.blocks) {
222
+ if (block.value) {
223
+ soul[block.label] = block.value;
224
+ }
225
+ }
226
+ return { soul };
227
+ }
228
+ // ─── Environments ────────────────────────────────────
229
+ async listEnvironments() {
230
+ return this.request("GET", "/environments");
231
+ }
232
+ // ─── Blocks ─────────────────────────────────────────
233
+ async listBlocks() {
234
+ return this.request("GET", "/blocks");
235
+ }
236
+ // ─── Export ─────────────────────────────────────────
237
+ async exportData(params) {
238
+ let path;
239
+ switch (params.type) {
240
+ case "conversation":
241
+ if (!params.conversation_id) throw new Error("conversation_id is required");
242
+ path = `/export/conversation/${params.conversation_id}?format=${params.format ?? "md"}`;
243
+ break;
244
+ case "memories":
245
+ path = `/export/memories?format=${params.format ?? "json"}`;
246
+ break;
247
+ case "soul":
248
+ path = `/export/soul?format=${params.format ?? "json"}`;
249
+ break;
250
+ case "all":
251
+ path = "/export/all";
252
+ break;
253
+ default:
254
+ throw new Error(`Unknown export type: ${params.type}`);
255
+ }
256
+ const url = `${this.config.baseUrl}${path}`;
257
+ const res = await this.fetchWithRetry(url, { method: "GET", headers: this.headers() });
258
+ if (!res.ok) throw new Error(`Export failed (${res.status})`);
259
+ return res.text();
260
+ }
261
+ // ─── Block Management ──────────────────────────────
262
+ async updateBlock(label, value) {
263
+ return this.request(
264
+ "PUT",
265
+ `/blocks/${encodeURIComponent(label)}`,
266
+ { value }
267
+ );
268
+ }
269
+ // ─── Memory Management ────────────────────────────
270
+ async deleteMemory(id) {
271
+ return this.request(
272
+ "DELETE",
273
+ `/memories/${encodeURIComponent(String(id))}`
274
+ );
275
+ }
276
+ async updateMemory(id, params) {
277
+ return this.request(
278
+ "PUT",
279
+ `/memories/${encodeURIComponent(String(id))}`,
280
+ params
281
+ );
282
+ }
283
+ // ─── Image Generation ────────────────────────────
284
+ async generateImage(params) {
285
+ return this.request("POST", "/images/generate", params);
286
+ }
287
+ // ─── Environment Management ───────────────────────
288
+ async createEnvironment(name) {
289
+ return this.request("POST", "/environments", { name });
290
+ }
291
+ // ─── Context Focus ───────────────────────────────
292
+ async focusContext(context, source) {
293
+ return this.request(
294
+ "POST",
295
+ "/context/focus",
296
+ {
297
+ context,
298
+ source: source ?? "mcp"
299
+ }
300
+ );
301
+ }
302
+ // ─── Enhanced Search ────────────────────────────────
303
+ async searchMemoriesAdvanced(queryString) {
304
+ return this.request("GET", `/memories/search?${queryString}`);
305
+ }
306
+ // ─── Episodes ───────────────────────────────────────
307
+ async listEpisodes(params) {
308
+ const qs = new URLSearchParams();
309
+ if (params?.limit != null) qs.set("limit", String(params.limit));
310
+ if (params?.offset != null) qs.set("offset", String(params.offset));
311
+ return this.request("GET", `/episodes?${qs}`);
312
+ }
313
+ async searchEpisodes(params) {
314
+ const qs = new URLSearchParams({ topic: params.topic });
315
+ if (params.limit) qs.set("limit", String(params.limit));
316
+ return this.request("GET", `/episodes/search?${qs}`);
317
+ }
318
+ async createEpisode(params) {
319
+ return this.request("POST", "/episodes", params);
320
+ }
321
+ // ─── Procedures ─────────────────────────────────────
322
+ async listProcedures(params) {
323
+ const qs = new URLSearchParams();
324
+ if (params?.limit != null) qs.set("limit", String(params.limit));
325
+ if (params?.offset != null) qs.set("offset", String(params.offset));
326
+ return this.request("GET", `/procedures?${qs}`);
327
+ }
328
+ async createProcedure(params) {
329
+ return this.request("POST", "/procedures", params);
330
+ }
331
+ };
332
+
333
+ // src/tools/assemble.ts
334
+ import { z } from "zod";
335
+ function registerAssembleTool(server2, client2) {
336
+ server2.registerTool(
337
+ "alma_assemble",
338
+ {
339
+ title: "Assemble Context",
340
+ description: "Build a complete system prompt from Alma \u2014 includes identity (soul), relevant memories, recent episodes, and learned procedures. Use this before sending a message to an LLM to give it full context about the user.",
341
+ inputSchema: {
342
+ message: z.string().min(1).describe("The user message to assemble context for"),
343
+ environment_id: z.string().optional().describe("Environment ID (uses default if omitted)"),
344
+ token_budget: z.number().int().positive().optional().describe("Maximum token budget for the assembled context (default: 2000)")
345
+ }
346
+ },
347
+ async ({ message, environment_id, token_budget }) => {
348
+ try {
349
+ const result = await client2.assembleContext({
350
+ message,
351
+ environment_id,
352
+ token_budget
353
+ });
354
+ return {
355
+ content: [
356
+ {
357
+ type: "text",
358
+ text: result.system_prompt
359
+ },
360
+ {
361
+ type: "text",
362
+ text: `
363
+ ---
364
+ Metadata: ${result.metadata.memoriesIncluded} memories, ${result.metadata.episodesIncluded} episodes, ${result.metadata.proceduresIncluded} procedures (${result.metadata.totalTokens} tokens)`
365
+ }
366
+ ]
367
+ };
368
+ } catch (err) {
369
+ const errorMessage = err instanceof Error ? err.message : "Unknown error";
370
+ return {
371
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
372
+ isError: true
373
+ };
374
+ }
375
+ }
376
+ );
377
+ }
378
+
379
+ // src/tools/remember.ts
380
+ import { z as z2 } from "zod";
381
+ function registerRememberTool(server2, client2) {
382
+ server2.registerTool(
383
+ "alma_remember",
384
+ {
385
+ title: "Remember",
386
+ description: "Save a new memory to Alma. Use this to store facts, preferences, decisions, or any information that should persist across conversations.",
387
+ inputSchema: {
388
+ content: z2.string().min(1).max(5e4).describe("The memory content to save"),
389
+ category: z2.enum(["general", "preference", "fact", "decision", "project"]).optional().describe("Category of the memory (default: general)"),
390
+ importance: z2.number().min(0).max(1).optional().describe("Importance from 0 to 1 (default: 0.5)")
391
+ }
392
+ },
393
+ async ({ content, category, importance }) => {
394
+ try {
395
+ const result = await client2.createMemory({
396
+ content,
397
+ category,
398
+ importance,
399
+ source: "mcp"
400
+ });
401
+ return {
402
+ content: [
403
+ {
404
+ type: "text",
405
+ text: `Memory saved successfully.
406
+ ${JSON.stringify(result.memory, null, 2)}`
407
+ }
408
+ ]
409
+ };
410
+ } catch (err) {
411
+ const message = err instanceof Error ? err.message : "Unknown error";
412
+ return {
413
+ content: [{ type: "text", text: `Error: ${message}` }],
414
+ isError: true
415
+ };
416
+ }
417
+ }
418
+ );
419
+ }
420
+
421
+ // src/tools/recall.ts
422
+ import { z as z3 } from "zod";
423
+ function registerRecallTool(server2, client2) {
424
+ server2.registerTool(
425
+ "alma_recall",
426
+ {
427
+ title: "Recall Memories",
428
+ description: "Search Alma memories by keyword. Returns relevant memories matching the query, ranked by relevance and importance. Use this to retrieve stored knowledge.",
429
+ inputSchema: {
430
+ query: z3.string().min(1).describe("Search query to find relevant memories"),
431
+ limit: z3.number().int().min(1).max(50).optional().describe("Maximum number of results (default: 10)")
432
+ }
433
+ },
434
+ async ({ query, limit }) => {
435
+ try {
436
+ const result = await client2.searchMemories({
437
+ q: query,
438
+ limit: limit ?? 10
439
+ });
440
+ if (result.memories.length === 0) {
441
+ return {
442
+ content: [
443
+ { type: "text", text: "No memories found matching the query." }
444
+ ]
445
+ };
446
+ }
447
+ const formatted = result.memories.map((m, i) => {
448
+ const cat = m.category ?? "unknown";
449
+ const imp = m.importance ?? "?";
450
+ return `${i + 1}. [${cat}] (importance: ${imp}) ${m.content}`;
451
+ }).join("\n");
452
+ return {
453
+ content: [
454
+ {
455
+ type: "text",
456
+ text: `Found ${result.memories.length} memories:
457
+
458
+ ${formatted}`
459
+ }
460
+ ]
461
+ };
462
+ } catch (err) {
463
+ const message = err instanceof Error ? err.message : "Unknown error";
464
+ return {
465
+ content: [{ type: "text", text: `Error: ${message}` }],
466
+ isError: true
467
+ };
468
+ }
469
+ }
470
+ );
471
+ }
472
+
473
+ // src/tools/extract.ts
474
+ import { z as z4 } from "zod";
475
+ function registerExtractTool(server2, client2) {
476
+ server2.registerTool(
477
+ "alma_extract",
478
+ {
479
+ title: "Extract Memories",
480
+ description: "Analyze a conversation and extract key memories, facts, and patterns using AI. Optionally saves them to Alma automatically. Use this after a conversation to capture important information.",
481
+ inputSchema: {
482
+ text: z4.string().min(10).max(2e5).describe("Conversation text to extract memories from"),
483
+ save: z4.boolean().optional().describe("Whether to automatically save extracted memories (default: true)"),
484
+ environment_id: z4.string().optional().describe("Environment ID (uses default if omitted)")
485
+ }
486
+ },
487
+ async ({ text, save, environment_id }) => {
488
+ try {
489
+ const result = await client2.extractMemories({
490
+ text,
491
+ save: save ?? true,
492
+ environment_id
493
+ });
494
+ const parts = [];
495
+ if (result.memories.length > 0) {
496
+ parts.push(`Extracted ${result.memories.length} memories:`);
497
+ for (const m of result.memories) {
498
+ parts.push(` - [${m.category}] (importance: ${m.importance}) ${m.content}`);
499
+ }
500
+ } else {
501
+ parts.push("No memories extracted from the text.");
502
+ }
503
+ if (result.episode) {
504
+ parts.push(`
505
+ Episode: ${result.episode.summary}`);
506
+ if (result.episode.topics.length > 0) {
507
+ parts.push(`Topics: ${result.episode.topics.join(", ")}`);
508
+ }
509
+ }
510
+ if (result.saved) {
511
+ parts.push("\nAll extracted data has been saved to Alma.");
512
+ }
513
+ return {
514
+ content: [{ type: "text", text: parts.join("\n") }]
515
+ };
516
+ } catch (err) {
517
+ const message = err instanceof Error ? err.message : "Unknown error";
518
+ return {
519
+ content: [{ type: "text", text: `Error: ${message}` }],
520
+ isError: true
521
+ };
522
+ }
523
+ }
524
+ );
525
+ }
526
+
527
+ // src/tools/chat.ts
528
+ import { z as z5 } from "zod";
529
+ function registerChatTool(server2, client2) {
530
+ server2.registerTool(
531
+ "alma_chat",
532
+ {
533
+ title: "Chat with Alma",
534
+ description: "Send a message to Alma's managed chat. Alma assembles context from memories, soul, and episodes, then responds via its own LLM. Creates a new conversation if conversation_id is not provided.",
535
+ inputSchema: {
536
+ message: z5.string().min(1).describe("The message to send"),
537
+ conversation_id: z5.string().optional().describe("Existing conversation ID. If omitted, a new conversation is created."),
538
+ environment_id: z5.string().optional().describe("Environment ID for new conversations (uses default if omitted)"),
539
+ model: z5.enum([
540
+ "claude-haiku",
541
+ "claude-sonnet",
542
+ "claude-opus"
543
+ ]).optional().describe("LLM model to use for the response. If omitted, uses the server default.")
544
+ }
545
+ },
546
+ async ({ message, conversation_id, environment_id, model }) => {
547
+ try {
548
+ let convId = conversation_id;
549
+ if (!convId) {
550
+ const conv = await client2.createConversation({
551
+ title: message.slice(0, 60),
552
+ environment_id
553
+ });
554
+ convId = conv.conversation.id;
555
+ }
556
+ const response = await client2.sendMessage(convId, message, model);
557
+ return {
558
+ content: [
559
+ {
560
+ type: "text",
561
+ text: response
562
+ },
563
+ {
564
+ type: "text",
565
+ text: `
566
+ ---
567
+ Conversation: ${convId}${model ? ` | Model: ${model}` : ""}`
568
+ }
569
+ ]
570
+ };
571
+ } catch (err) {
572
+ const errMsg = err instanceof Error ? err.message : "Unknown error";
573
+ return {
574
+ content: [{ type: "text", text: `Error: ${errMsg}` }],
575
+ isError: true
576
+ };
577
+ }
578
+ }
579
+ );
580
+ }
581
+
582
+ // src/tools/generate-image.ts
583
+ import { z as z6 } from "zod";
584
+ function registerGenerateImageTool(server2, client2) {
585
+ server2.registerTool(
586
+ "alma_generate_image",
587
+ {
588
+ title: "Generate Image",
589
+ description: "Generate an image using Alma's Image Studio (Replicate Flux or Leonardo AI). Returns the generated image URL with metadata. Requires a paid plan.",
590
+ inputSchema: {
591
+ prompt: z6.string().min(1).max(2e3).describe("Text description of the image to generate"),
592
+ style: z6.enum(["natural", "vivid", "artistic"]).optional().describe("Image style (default: natural)"),
593
+ size: z6.enum(["square", "landscape", "portrait"]).optional().describe("Image size/aspect ratio (default: square)")
594
+ }
595
+ },
596
+ async ({ prompt, style, size }) => {
597
+ try {
598
+ const result = await client2.generateImage({ prompt, style, size });
599
+ const img = result.image;
600
+ return {
601
+ content: [
602
+ {
603
+ type: "text",
604
+ text: `Image generated successfully.
605
+
606
+ URL: ${img.url}
607
+ Size: ${img.width}x${img.height}
608
+ Model: ${img.model} (${img.provider})
609
+ Prompt: ${img.prompt}`
610
+ }
611
+ ]
612
+ };
613
+ } catch (err) {
614
+ const message = err instanceof Error ? err.message : "Unknown error";
615
+ return {
616
+ content: [{ type: "text", text: `Error: ${message}` }],
617
+ isError: true
618
+ };
619
+ }
620
+ }
621
+ );
622
+ }
623
+
624
+ // src/tools/export.ts
625
+ import { z as z7 } from "zod";
626
+ function registerExportTool(server2, client2) {
627
+ server2.registerTool(
628
+ "alma_export",
629
+ {
630
+ title: "Export Data",
631
+ description: "Export Alma data in text formats. Supported exports: conversation (md or html), memories (json or md), soul (json or md), or all (json). Binary formats (pdf, docx, xlsx) are only available via the web app.",
632
+ inputSchema: {
633
+ type: z7.enum(["conversation", "memories", "soul", "all"]).describe("What to export"),
634
+ format: z7.enum(["md", "json", "html"]).optional().describe('Export format. For conversation: md (default) or html. For memories/soul: json (default) or md. Ignored for "all".'),
635
+ conversation_id: z7.string().optional().describe('Conversation ID (required when type is "conversation")')
636
+ }
637
+ },
638
+ async ({ type, format, conversation_id }) => {
639
+ try {
640
+ if (type === "conversation" && format && !["md", "html"].includes(format)) {
641
+ return {
642
+ content: [{ type: "text", text: 'Error: Conversation export supports "md" or "html" format only.' }],
643
+ isError: true
644
+ };
645
+ }
646
+ if ((type === "memories" || type === "soul") && format && !["json", "md"].includes(format)) {
647
+ return {
648
+ content: [{ type: "text", text: `Error: ${type} export supports "json" or "md" format only.` }],
649
+ isError: true
650
+ };
651
+ }
652
+ const result = await client2.exportData({ type, format, conversation_id });
653
+ return {
654
+ content: [
655
+ {
656
+ type: "text",
657
+ text: result
658
+ }
659
+ ]
660
+ };
661
+ } catch (err) {
662
+ const message = err instanceof Error ? err.message : "Unknown error";
663
+ return {
664
+ content: [{ type: "text", text: `Error: ${message}` }],
665
+ isError: true
666
+ };
667
+ }
668
+ }
669
+ );
670
+ }
671
+
672
+ // src/tools/search.ts
673
+ import { z as z8 } from "zod";
674
+ function registerSearchTool(server2, client2) {
675
+ server2.registerTool(
676
+ "alma_search",
677
+ {
678
+ title: "Enhanced Search",
679
+ description: "Search Alma memories with advanced filters: date range, category, importance threshold, and search mode (keyword, semantic, or hybrid). More powerful than basic recall.",
680
+ inputSchema: {
681
+ query: z8.string().min(1).describe("Search query"),
682
+ mode: z8.enum(["keyword", "semantic", "hybrid"]).optional().describe("Search mode (default: keyword)"),
683
+ from: z8.string().optional().describe("Start date filter (ISO format, e.g. 2025-01-01)"),
684
+ to: z8.string().optional().describe("End date filter (ISO format)"),
685
+ category: z8.enum(["general", "preference", "fact", "decision", "project"]).optional().describe("Filter by memory category"),
686
+ min_importance: z8.number().min(0).max(1).optional().describe("Minimum importance threshold (0-1)"),
687
+ limit: z8.number().int().min(1).max(50).optional().describe("Maximum results (default: 20)")
688
+ }
689
+ },
690
+ async ({ query, mode, from, to, category, min_importance, limit }) => {
691
+ try {
692
+ const qs = new URLSearchParams({ q: query });
693
+ if (mode) qs.set("mode", mode);
694
+ if (from) qs.set("from", from);
695
+ if (to) qs.set("to", to);
696
+ if (category) qs.set("category", category);
697
+ if (min_importance !== void 0) qs.set("min_importance", String(min_importance));
698
+ if (limit) qs.set("limit", String(limit));
699
+ const result = await client2.searchMemoriesAdvanced(qs.toString());
700
+ if (result.memories.length === 0) {
701
+ return {
702
+ content: [
703
+ { type: "text", text: "No memories found matching the search criteria." }
704
+ ]
705
+ };
706
+ }
707
+ const formatted = result.memories.map((m, i) => {
708
+ const score = m.score ? ` (score: ${m.score.toFixed(3)})` : "";
709
+ return `${i + 1}. [${m.category}] (importance: ${m.importance})${score} ${m.content}`;
710
+ }).join("\n");
711
+ return {
712
+ content: [
713
+ {
714
+ type: "text",
715
+ text: `Found ${result.memories.length} memories (mode: ${mode ?? "keyword"}):
716
+
717
+ ${formatted}`
718
+ }
719
+ ]
720
+ };
721
+ } catch (err) {
722
+ const message = err instanceof Error ? err.message : "Unknown error";
723
+ return {
724
+ content: [{ type: "text", text: `Error: ${message}` }],
725
+ isError: true
726
+ };
727
+ }
728
+ }
729
+ );
730
+ }
731
+
732
+ // src/tools/update-block.ts
733
+ import { z as z9 } from "zod";
734
+ function registerUpdateBlockTool(server2, client2) {
735
+ server2.registerTool(
736
+ "alma_update_block",
737
+ {
738
+ title: "Update Block",
739
+ description: "Update a memory block value (e.g. identity, rules, language, user_profile, active_context, learned_patterns, scratchpad). Use this to modify Alma's personality and behavior configuration.",
740
+ inputSchema: {
741
+ label: z9.string().min(1).describe('The block label to update (e.g. "identity", "rules", "language", "user_profile")'),
742
+ value: z9.string().min(1).describe("The new value for the block")
743
+ }
744
+ },
745
+ async ({ label, value }) => {
746
+ try {
747
+ const result = await client2.updateBlock(label, value);
748
+ return {
749
+ content: [
750
+ {
751
+ type: "text",
752
+ text: `Block "${label}" updated successfully.
753
+ ${JSON.stringify(result.block, null, 2)}`
754
+ }
755
+ ]
756
+ };
757
+ } catch (err) {
758
+ const message = err instanceof Error ? err.message : "Unknown error";
759
+ return {
760
+ content: [{ type: "text", text: `Error: ${message}` }],
761
+ isError: true
762
+ };
763
+ }
764
+ }
765
+ );
766
+ }
767
+
768
+ // src/tools/manage-memory.ts
769
+ import { z as z10 } from "zod";
770
+ function registerDeleteMemoryTool(server2, client2) {
771
+ server2.registerTool(
772
+ "alma_delete_memory",
773
+ {
774
+ title: "Delete Memory",
775
+ description: "Delete a specific memory by its ID. Use alma_recall or alma_search first to find the memory ID.",
776
+ inputSchema: {
777
+ id: z10.string().min(1).regex(/^\d+$/, "Memory ID must be numeric").describe("The memory ID to delete")
778
+ }
779
+ },
780
+ async ({ id }) => {
781
+ try {
782
+ await client2.deleteMemory(Number(id));
783
+ return {
784
+ content: [
785
+ {
786
+ type: "text",
787
+ text: `Memory ${id} deleted successfully.`
788
+ }
789
+ ]
790
+ };
791
+ } catch (err) {
792
+ const message = err instanceof Error ? err.message : "Unknown error";
793
+ return {
794
+ content: [{ type: "text", text: `Error: ${message}` }],
795
+ isError: true
796
+ };
797
+ }
798
+ }
799
+ );
800
+ }
801
+
802
+ // src/tools/environment.ts
803
+ import { z as z11 } from "zod";
804
+ function registerCreateEnvironmentTool(server2, client2) {
805
+ server2.registerTool(
806
+ "alma_create_environment",
807
+ {
808
+ title: "Create Environment",
809
+ description: "Create a new Alma environment. Environments isolate soul configuration, memories, and conversations for different projects or contexts.",
810
+ inputSchema: {
811
+ name: z11.string().min(1).max(100).describe('Name for the new environment (e.g. "Work Project", "Personal")')
812
+ }
813
+ },
814
+ async ({ name }) => {
815
+ try {
816
+ const result = await client2.createEnvironment(name);
817
+ return {
818
+ content: [
819
+ {
820
+ type: "text",
821
+ text: `Environment created successfully.
822
+ ${JSON.stringify(result.environment, null, 2)}`
823
+ }
824
+ ]
825
+ };
826
+ } catch (err) {
827
+ const message = err instanceof Error ? err.message : "Unknown error";
828
+ return {
829
+ content: [{ type: "text", text: `Error: ${message}` }],
830
+ isError: true
831
+ };
832
+ }
833
+ }
834
+ );
835
+ }
836
+ function registerListEnvironmentsTool(server2, client2) {
837
+ server2.registerTool(
838
+ "alma_list_environments",
839
+ {
840
+ title: "List Environments",
841
+ description: "List available environments. Use the returned environment IDs to scope subsequent tool calls with the environment_id parameter.",
842
+ inputSchema: {}
843
+ },
844
+ async () => {
845
+ try {
846
+ const result = await client2.listEnvironments();
847
+ const envList = result.environments.map((e) => `- ${e.name} (${e.id})${e.is_default ? " [default]" : ""}`).join("\n");
848
+ return {
849
+ content: [
850
+ {
851
+ type: "text",
852
+ text: `Available environments:
853
+ ${envList}
854
+
855
+ Use the environment_id parameter in other tools to work within a specific environment.`
856
+ }
857
+ ]
858
+ };
859
+ } catch (err) {
860
+ const message = err instanceof Error ? err.message : "Unknown error";
861
+ return {
862
+ content: [{ type: "text", text: `Error: ${message}` }],
863
+ isError: true
864
+ };
865
+ }
866
+ }
867
+ );
868
+ }
869
+
870
+ // src/tools/focus.ts
871
+ import { z as z12 } from "zod";
872
+ function registerFocusTool(server2, client2) {
873
+ server2.registerTool(
874
+ "alma_focus",
875
+ {
876
+ title: "Set Context Focus",
877
+ description: "Update Alma's active context with what you're currently working on. This helps Alma provide more relevant responses by knowing your current focus (e.g. project name, file, task).",
878
+ inputSchema: {
879
+ context: z12.string().min(1).max(2e3).describe('Description of the current focus/context (e.g. "Working on auth module in project-x", "Reviewing PR #42")'),
880
+ source: z12.enum(["web", "extension", "mcp", "api"]).optional().describe('Source identifier (default: "mcp")')
881
+ }
882
+ },
883
+ async ({ context, source }) => {
884
+ try {
885
+ await client2.focusContext(context, source);
886
+ return {
887
+ content: [
888
+ {
889
+ type: "text",
890
+ text: `Context focus updated: "${context}"`
891
+ }
892
+ ]
893
+ };
894
+ } catch (err) {
895
+ const message = err instanceof Error ? err.message : "Unknown error";
896
+ return {
897
+ content: [{ type: "text", text: `Error: ${message}` }],
898
+ isError: true
899
+ };
900
+ }
901
+ }
902
+ );
903
+ }
904
+
905
+ // src/tools/continue-session.ts
906
+ import { z as z13 } from "zod";
907
+ function registerContinueSessionTool(server2, client2) {
908
+ server2.registerTool(
909
+ "alma_continue_session",
910
+ {
911
+ title: "Continue Session",
912
+ description: "Generate a session continuation summary when hitting context limits or starting a new session. Returns a structured summary with active tasks, key decisions, and a continuation prompt.",
913
+ inputSchema: {
914
+ conversation_text: z13.string().min(1).describe("The conversation text to summarize for continuation"),
915
+ reason: z13.enum(["context_limit", "new_session", "error"]).describe("Why the session is being continued"),
916
+ environment_id: z13.string().optional().describe("Environment ID to scope the continuation")
917
+ }
918
+ },
919
+ async ({ conversation_text, reason, environment_id }) => {
920
+ try {
921
+ const result = await client2.continueSession({
922
+ conversation_text,
923
+ reason,
924
+ environment_id
925
+ });
926
+ return {
927
+ content: [
928
+ {
929
+ type: "text",
930
+ text: [
931
+ "## Session Summary",
932
+ result.session_summary,
933
+ "",
934
+ "## Active Tasks",
935
+ ...result.active_tasks.map((t) => `- ${t}`),
936
+ "",
937
+ "## Key Decisions",
938
+ ...result.key_decisions.map((d) => `- ${d}`),
939
+ "",
940
+ "## Pending Items",
941
+ ...result.pending_items.map((p) => `- ${p}`),
942
+ "",
943
+ "## Continuation Prompt",
944
+ result.continuation_prompt
945
+ ].join("\n")
946
+ }
947
+ ]
948
+ };
949
+ } catch (err) {
950
+ const message = err instanceof Error ? err.message : "Unknown error";
951
+ return {
952
+ content: [{ type: "text", text: `Error: ${message}` }],
953
+ isError: true
954
+ };
955
+ }
956
+ }
957
+ );
958
+ }
959
+
960
+ // src/tools/episodes.ts
961
+ import { z as z14 } from "zod";
962
+ function registerListEpisodesTool(server2, client2) {
963
+ server2.registerTool(
964
+ "alma_list_episodes",
965
+ {
966
+ title: "List Episodes",
967
+ description: "List recent episodes (conversation summaries with topics and outcomes). Episodes capture what happened in past conversations.",
968
+ inputSchema: {
969
+ limit: z14.number().int().min(1).max(100).optional().describe("Maximum number of episodes to return (default: 20)"),
970
+ offset: z14.number().int().min(0).optional().describe("Number of episodes to skip (default: 0)")
971
+ }
972
+ },
973
+ async ({ limit, offset }) => {
974
+ try {
975
+ const result = await client2.listEpisodes({ limit, offset });
976
+ if (result.episodes.length === 0) {
977
+ return {
978
+ content: [{ type: "text", text: "No episodes found." }]
979
+ };
980
+ }
981
+ return {
982
+ content: [
983
+ {
984
+ type: "text",
985
+ text: JSON.stringify(result, null, 2)
986
+ }
987
+ ]
988
+ };
989
+ } catch (err) {
990
+ const message = err instanceof Error ? err.message : "Unknown error";
991
+ return {
992
+ content: [{ type: "text", text: `Error: ${message}` }],
993
+ isError: true
994
+ };
995
+ }
996
+ }
997
+ );
998
+ }
999
+ function registerSearchEpisodesTool(server2, client2) {
1000
+ server2.registerTool(
1001
+ "alma_search_episodes",
1002
+ {
1003
+ title: "Search Episodes",
1004
+ description: "Search episodes by topic. Returns episodes that match the given topic keyword.",
1005
+ inputSchema: {
1006
+ topic: z14.string().min(1).describe("Topic keyword to search for"),
1007
+ limit: z14.number().int().min(1).max(50).optional().describe("Maximum number of results (default: 10)")
1008
+ }
1009
+ },
1010
+ async ({ topic, limit }) => {
1011
+ try {
1012
+ const result = await client2.searchEpisodes({ topic, limit });
1013
+ if (result.episodes.length === 0) {
1014
+ return {
1015
+ content: [{ type: "text", text: `No episodes found for topic: ${topic}` }]
1016
+ };
1017
+ }
1018
+ return {
1019
+ content: [
1020
+ {
1021
+ type: "text",
1022
+ text: JSON.stringify(result, null, 2)
1023
+ }
1024
+ ]
1025
+ };
1026
+ } catch (err) {
1027
+ const message = err instanceof Error ? err.message : "Unknown error";
1028
+ return {
1029
+ content: [{ type: "text", text: `Error: ${message}` }],
1030
+ isError: true
1031
+ };
1032
+ }
1033
+ }
1034
+ );
1035
+ }
1036
+ function registerCreateEpisodeTool(server2, client2) {
1037
+ server2.registerTool(
1038
+ "alma_create_episode",
1039
+ {
1040
+ title: "Create Episode",
1041
+ description: "Create a new episode (conversation summary). Episodes capture what happened, what topics were discussed, and the outcome.",
1042
+ inputSchema: {
1043
+ summary: z14.string().min(1).max(5e3).describe("Summary of what happened in the conversation"),
1044
+ topics: z14.array(z14.string()).optional().describe("List of topics discussed"),
1045
+ outcome: z14.string().optional().describe("Outcome or result of the conversation")
1046
+ }
1047
+ },
1048
+ async ({ summary, topics, outcome }) => {
1049
+ try {
1050
+ const result = await client2.createEpisode({ summary, topics, outcome });
1051
+ return {
1052
+ content: [
1053
+ {
1054
+ type: "text",
1055
+ text: `Episode created successfully.
1056
+ ${JSON.stringify(result, null, 2)}`
1057
+ }
1058
+ ]
1059
+ };
1060
+ } catch (err) {
1061
+ const message = err instanceof Error ? err.message : "Unknown error";
1062
+ return {
1063
+ content: [{ type: "text", text: `Error: ${message}` }],
1064
+ isError: true
1065
+ };
1066
+ }
1067
+ }
1068
+ );
1069
+ }
1070
+
1071
+ // src/tools/procedures.ts
1072
+ import { z as z15 } from "zod";
1073
+ function registerListProceduresTool(server2, client2) {
1074
+ server2.registerTool(
1075
+ "alma_list_procedures",
1076
+ {
1077
+ title: "List Procedures",
1078
+ description: "List stored procedures. Procedures are trigger-action rules that Alma follows when certain patterns are detected in conversations.",
1079
+ inputSchema: {
1080
+ limit: z15.number().int().min(1).max(100).optional().describe("Maximum number of procedures to return (default: 50)"),
1081
+ offset: z15.number().int().min(0).optional().describe("Number of procedures to skip (default: 0)")
1082
+ }
1083
+ },
1084
+ async ({ limit, offset }) => {
1085
+ try {
1086
+ const result = await client2.listProcedures({ limit, offset });
1087
+ if (result.procedures.length === 0) {
1088
+ return {
1089
+ content: [{ type: "text", text: "No procedures found." }]
1090
+ };
1091
+ }
1092
+ return {
1093
+ content: [
1094
+ {
1095
+ type: "text",
1096
+ text: JSON.stringify(result, null, 2)
1097
+ }
1098
+ ]
1099
+ };
1100
+ } catch (err) {
1101
+ const message = err instanceof Error ? err.message : "Unknown error";
1102
+ return {
1103
+ content: [{ type: "text", text: `Error: ${message}` }],
1104
+ isError: true
1105
+ };
1106
+ }
1107
+ }
1108
+ );
1109
+ }
1110
+ function registerCreateProcedureTool(server2, client2) {
1111
+ server2.registerTool(
1112
+ "alma_create_procedure",
1113
+ {
1114
+ title: "Create Procedure",
1115
+ description: "Create a new procedure (trigger-action rule). Procedures define automated behaviors: when a trigger pattern is detected, the specified action is taken.",
1116
+ inputSchema: {
1117
+ trigger_pattern: z15.string().min(1).describe("The pattern or condition that triggers this procedure"),
1118
+ action: z15.string().min(1).describe("The action to take when the trigger is detected"),
1119
+ context: z15.string().optional().describe("Additional context or conditions for the procedure")
1120
+ }
1121
+ },
1122
+ async ({ trigger_pattern, action, context }) => {
1123
+ try {
1124
+ const result = await client2.createProcedure({ trigger_pattern, action, context });
1125
+ return {
1126
+ content: [
1127
+ {
1128
+ type: "text",
1129
+ text: `Procedure created successfully.
1130
+ ${JSON.stringify(result, null, 2)}`
1131
+ }
1132
+ ]
1133
+ };
1134
+ } catch (err) {
1135
+ const message = err instanceof Error ? err.message : "Unknown error";
1136
+ return {
1137
+ content: [{ type: "text", text: `Error: ${message}` }],
1138
+ isError: true
1139
+ };
1140
+ }
1141
+ }
1142
+ );
1143
+ }
1144
+
1145
+ // src/tools/preview-context.ts
1146
+ import { z as z16 } from "zod";
1147
+ function registerPreviewContextTool(server2, client2) {
1148
+ server2.registerTool(
1149
+ "alma_preview_context",
1150
+ {
1151
+ title: "Preview Context",
1152
+ description: "Preview the full system prompt that Alma would assemble for a given message. Shows exactly what the LLM sees, including identity, memories, episodes, and procedures. Useful for debugging and understanding context assembly.",
1153
+ inputSchema: {
1154
+ message: z16.string().optional().describe("Optional message to simulate context assembly for. If omitted, shows the default context.")
1155
+ }
1156
+ },
1157
+ async ({ message }) => {
1158
+ try {
1159
+ const result = await client2.previewContext(message);
1160
+ const metaSummary = result.metadata ? `
1161
+
1162
+ --- Metadata ---
1163
+ Soul tokens: ${result.metadata.soulTokens}
1164
+ Memories: ${result.metadata.memoriesIncluded} (${result.metadata.memoriesTokens} tokens)
1165
+ Episodes: ${result.metadata.episodesIncluded} (${result.metadata.episodesTokens} tokens)
1166
+ Procedures: ${result.metadata.proceduresIncluded} (${result.metadata.proceduresTokens} tokens)
1167
+ Total tokens: ${result.metadata.totalTokens}` : "";
1168
+ return {
1169
+ content: [
1170
+ {
1171
+ type: "text",
1172
+ text: `${result.system_prompt}${metaSummary}`
1173
+ }
1174
+ ]
1175
+ };
1176
+ } catch (err) {
1177
+ const errorMessage = err instanceof Error ? err.message : "Unknown error";
1178
+ return {
1179
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
1180
+ isError: true
1181
+ };
1182
+ }
1183
+ }
1184
+ );
1185
+ }
1186
+
1187
+ // src/tools/update-memory.ts
1188
+ import { z as z17 } from "zod";
1189
+ function registerUpdateMemoryTool(server2, client2) {
1190
+ server2.registerTool(
1191
+ "alma_update_memory",
1192
+ {
1193
+ title: "Update Memory",
1194
+ description: "Update an existing memory in Alma. Can modify the content, category, or importance. At least one field must be provided.",
1195
+ inputSchema: {
1196
+ id: z17.number().int().positive().describe("The memory ID to update"),
1197
+ content: z17.string().min(1).max(5e4).optional().describe("New content for the memory"),
1198
+ category: z17.enum(["general", "preference", "fact", "decision", "project"]).optional().describe("New category for the memory"),
1199
+ importance: z17.number().min(0).max(1).optional().describe("New importance score (0 to 1)")
1200
+ }
1201
+ },
1202
+ async ({ id, content, category, importance }) => {
1203
+ try {
1204
+ if (content === void 0 && category === void 0 && importance === void 0) {
1205
+ return {
1206
+ content: [{ type: "text", text: "Error: At least one of content, category, or importance must be provided." }],
1207
+ isError: true
1208
+ };
1209
+ }
1210
+ const params = {};
1211
+ if (content !== void 0) params.content = content;
1212
+ if (category !== void 0) params.category = category;
1213
+ if (importance !== void 0) params.importance = importance;
1214
+ const result = await client2.updateMemory(id, params);
1215
+ return {
1216
+ content: [
1217
+ {
1218
+ type: "text",
1219
+ text: `Memory ${id} updated successfully.
1220
+ ${JSON.stringify(result.memory, null, 2)}`
1221
+ }
1222
+ ]
1223
+ };
1224
+ } catch (err) {
1225
+ const message = err instanceof Error ? err.message : "Unknown error";
1226
+ return {
1227
+ content: [{ type: "text", text: `Error: ${message}` }],
1228
+ isError: true
1229
+ };
1230
+ }
1231
+ }
1232
+ );
1233
+ }
1234
+
1235
+ // src/index.ts
1236
+ var config = loadConfig();
1237
+ var client = new AlmaClient(config);
1238
+ var server = new McpServer({
1239
+ name: "alma-mcp",
1240
+ version: "1.3.0"
1241
+ });
1242
+ registerAssembleTool(server, client);
1243
+ registerRememberTool(server, client);
1244
+ registerRecallTool(server, client);
1245
+ registerExtractTool(server, client);
1246
+ registerChatTool(server, client);
1247
+ registerGenerateImageTool(server, client);
1248
+ registerExportTool(server, client);
1249
+ registerSearchTool(server, client);
1250
+ registerUpdateBlockTool(server, client);
1251
+ registerDeleteMemoryTool(server, client);
1252
+ registerCreateEnvironmentTool(server, client);
1253
+ registerListEnvironmentsTool(server, client);
1254
+ registerFocusTool(server, client);
1255
+ registerContinueSessionTool(server, client);
1256
+ registerListEpisodesTool(server, client);
1257
+ registerSearchEpisodesTool(server, client);
1258
+ registerCreateEpisodeTool(server, client);
1259
+ registerListProceduresTool(server, client);
1260
+ registerCreateProcedureTool(server, client);
1261
+ registerPreviewContextTool(server, client);
1262
+ registerUpdateMemoryTool(server, client);
1263
+ server.registerResource(
1264
+ "soul",
1265
+ "alma://soul",
1266
+ {
1267
+ title: "Alma Soul",
1268
+ description: "The current soul configuration (identity, personality, tone, rules)",
1269
+ mimeType: "application/json"
1270
+ },
1271
+ async (uri) => {
1272
+ try {
1273
+ const result = await client.getSoul();
1274
+ return {
1275
+ contents: [
1276
+ {
1277
+ uri: uri.href,
1278
+ mimeType: "application/json",
1279
+ text: JSON.stringify(result.soul, null, 2)
1280
+ }
1281
+ ]
1282
+ };
1283
+ } catch (err) {
1284
+ const message = err instanceof Error ? err.message : "Unknown error";
1285
+ return {
1286
+ contents: [{ uri: uri.href, mimeType: "text/plain", text: `Error loading resource: ${message}` }]
1287
+ };
1288
+ }
1289
+ }
1290
+ );
1291
+ server.registerResource(
1292
+ "memories",
1293
+ "alma://memories",
1294
+ {
1295
+ title: "Alma Memories",
1296
+ description: "Recent memories stored in Alma (last 50)",
1297
+ mimeType: "application/json"
1298
+ },
1299
+ async (uri) => {
1300
+ try {
1301
+ const result = await client.listMemories({ limit: 50 });
1302
+ return {
1303
+ contents: [
1304
+ {
1305
+ uri: uri.href,
1306
+ mimeType: "application/json",
1307
+ text: JSON.stringify(result.memories, null, 2)
1308
+ }
1309
+ ]
1310
+ };
1311
+ } catch (err) {
1312
+ const message = err instanceof Error ? err.message : "Unknown error";
1313
+ return {
1314
+ contents: [{ uri: uri.href, mimeType: "text/plain", text: `Error loading resource: ${message}` }]
1315
+ };
1316
+ }
1317
+ }
1318
+ );
1319
+ server.registerResource(
1320
+ "environments",
1321
+ "alma://environments",
1322
+ {
1323
+ title: "Alma Environments",
1324
+ description: "All environments (Alma instances) available to the user",
1325
+ mimeType: "application/json"
1326
+ },
1327
+ async (uri) => {
1328
+ try {
1329
+ const result = await client.listEnvironments();
1330
+ return {
1331
+ contents: [
1332
+ {
1333
+ uri: uri.href,
1334
+ mimeType: "application/json",
1335
+ text: JSON.stringify(result.environments, null, 2)
1336
+ }
1337
+ ]
1338
+ };
1339
+ } catch (err) {
1340
+ const message = err instanceof Error ? err.message : "Unknown error";
1341
+ return {
1342
+ contents: [{ uri: uri.href, mimeType: "text/plain", text: `Error loading resource: ${message}` }]
1343
+ };
1344
+ }
1345
+ }
1346
+ );
1347
+ server.registerResource(
1348
+ "conversations",
1349
+ "alma://conversations",
1350
+ {
1351
+ title: "Alma Conversations",
1352
+ description: "Recent chat conversations",
1353
+ mimeType: "application/json"
1354
+ },
1355
+ async (uri) => {
1356
+ try {
1357
+ const result = await client.listConversations({ limit: 20 });
1358
+ return {
1359
+ contents: [
1360
+ {
1361
+ uri: uri.href,
1362
+ mimeType: "application/json",
1363
+ text: JSON.stringify(result.conversations, null, 2)
1364
+ }
1365
+ ]
1366
+ };
1367
+ } catch (err) {
1368
+ const message = err instanceof Error ? err.message : "Unknown error";
1369
+ return {
1370
+ contents: [{ uri: uri.href, mimeType: "text/plain", text: `Error loading resource: ${message}` }]
1371
+ };
1372
+ }
1373
+ }
1374
+ );
1375
+ server.registerResource(
1376
+ "budget",
1377
+ "alma://budget",
1378
+ {
1379
+ title: "Token Budget",
1380
+ description: "Current token budget status (weekly usage, remaining, purchased)",
1381
+ mimeType: "application/json"
1382
+ },
1383
+ async (uri) => {
1384
+ try {
1385
+ const result = await client.getBudget();
1386
+ return {
1387
+ contents: [
1388
+ {
1389
+ uri: uri.href,
1390
+ mimeType: "application/json",
1391
+ text: JSON.stringify(result, null, 2)
1392
+ }
1393
+ ]
1394
+ };
1395
+ } catch (err) {
1396
+ const message = err instanceof Error ? err.message : "Unknown error";
1397
+ return {
1398
+ contents: [{ uri: uri.href, mimeType: "text/plain", text: `Error loading resource: ${message}` }]
1399
+ };
1400
+ }
1401
+ }
1402
+ );
1403
+ server.registerResource(
1404
+ "memories-by-category",
1405
+ new ResourceTemplate("alma://memories/{category}", {
1406
+ list: async () => ({
1407
+ resources: [
1408
+ { uri: "alma://memories/general", name: "General" },
1409
+ { uri: "alma://memories/preference", name: "Preferences" },
1410
+ { uri: "alma://memories/fact", name: "Facts" },
1411
+ { uri: "alma://memories/decision", name: "Decisions" },
1412
+ { uri: "alma://memories/project", name: "Projects" }
1413
+ ]
1414
+ })
1415
+ }),
1416
+ {
1417
+ title: "Memories by Category",
1418
+ description: "Alma memories filtered by category",
1419
+ mimeType: "application/json"
1420
+ },
1421
+ async (uri, { category }) => {
1422
+ try {
1423
+ const cat = Array.isArray(category) ? category[0] : category;
1424
+ const result = await client.listMemories({ category: cat, limit: 50 });
1425
+ return {
1426
+ contents: [
1427
+ {
1428
+ uri: uri.href,
1429
+ mimeType: "application/json",
1430
+ text: JSON.stringify(result.memories, null, 2)
1431
+ }
1432
+ ]
1433
+ };
1434
+ } catch (err) {
1435
+ const message = err instanceof Error ? err.message : "Unknown error";
1436
+ return {
1437
+ contents: [{ uri: uri.href, mimeType: "text/plain", text: `Error loading resource: ${message}` }]
1438
+ };
1439
+ }
1440
+ }
1441
+ );
1442
+ server.registerResource(
1443
+ "blocks",
1444
+ "alma://blocks",
1445
+ {
1446
+ title: "Memory Blocks",
1447
+ description: "All memory blocks (identity, worldview, style, rules, etc.)",
1448
+ mimeType: "application/json"
1449
+ },
1450
+ async (uri) => {
1451
+ try {
1452
+ const result = await client.listBlocks();
1453
+ return {
1454
+ contents: [
1455
+ {
1456
+ uri: uri.href,
1457
+ mimeType: "application/json",
1458
+ text: JSON.stringify(result.blocks, null, 2)
1459
+ }
1460
+ ]
1461
+ };
1462
+ } catch (err) {
1463
+ const message = err instanceof Error ? err.message : "Unknown error";
1464
+ return {
1465
+ contents: [{ uri: uri.href, mimeType: "text/plain", text: `Error loading resource: ${message}` }]
1466
+ };
1467
+ }
1468
+ }
1469
+ );
1470
+ server.registerResource(
1471
+ "episodes",
1472
+ "alma://episodes",
1473
+ {
1474
+ title: "Alma Episodes",
1475
+ description: "Recent episodes \u2014 conversation summaries with topics and outcomes (last 20)",
1476
+ mimeType: "application/json"
1477
+ },
1478
+ async (uri) => {
1479
+ try {
1480
+ const result = await client.listEpisodes({ limit: 20 });
1481
+ return {
1482
+ contents: [
1483
+ {
1484
+ uri: uri.href,
1485
+ mimeType: "application/json",
1486
+ text: JSON.stringify(result.episodes, null, 2)
1487
+ }
1488
+ ]
1489
+ };
1490
+ } catch (err) {
1491
+ const message = err instanceof Error ? err.message : "Unknown error";
1492
+ return {
1493
+ contents: [{ uri: uri.href, mimeType: "text/plain", text: `Error loading resource: ${message}` }]
1494
+ };
1495
+ }
1496
+ }
1497
+ );
1498
+ server.registerResource(
1499
+ "procedures",
1500
+ "alma://procedures",
1501
+ {
1502
+ title: "Alma Procedures",
1503
+ description: "All stored procedures \u2014 trigger-action rules that Alma follows (limit 50)",
1504
+ mimeType: "application/json"
1505
+ },
1506
+ async (uri) => {
1507
+ try {
1508
+ const result = await client.listProcedures({ limit: 50 });
1509
+ return {
1510
+ contents: [
1511
+ {
1512
+ uri: uri.href,
1513
+ mimeType: "application/json",
1514
+ text: JSON.stringify(result.procedures, null, 2)
1515
+ }
1516
+ ]
1517
+ };
1518
+ } catch (err) {
1519
+ const message = err instanceof Error ? err.message : "Unknown error";
1520
+ return {
1521
+ contents: [{ uri: uri.href, mimeType: "text/plain", text: `Error loading resource: ${message}` }]
1522
+ };
1523
+ }
1524
+ }
1525
+ );
1526
+ async function main() {
1527
+ const transport = new StdioServerTransport();
1528
+ await server.connect(transport);
1529
+ console.error("[alma-mcp] Server started on stdio (v1.3.0, 21 tools, 9 resources)");
1530
+ if (process.env.ALMA_DEBUG) {
1531
+ console.error(`[alma-mcp] API: ${config.baseUrl}`);
1532
+ if (config.environmentId) {
1533
+ console.error(`[alma-mcp] Environment: ${config.environmentId}`);
1534
+ }
1535
+ }
1536
+ }
1537
+ var shutdown = () => {
1538
+ console.error("[alma-mcp] Shutting down...");
1539
+ server.close().catch(() => {
1540
+ });
1541
+ process.exit(0);
1542
+ };
1543
+ process.on("SIGTERM", shutdown);
1544
+ process.on("SIGINT", shutdown);
1545
+ main().catch((err) => {
1546
+ console.error("[alma-mcp] Fatal error:", err);
1547
+ process.exit(1);
1548
+ });