index.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698
  1. #!/usr/bin/env node
  2. import { Server } from '@modelcontextprotocol/sdk/server/index.js';
  3. import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
  4. import {
  5. ListToolsRequestSchema,
  6. CallToolRequestSchema,
  7. } from '@modelcontextprotocol/sdk/types.js';
  8. import { execSync } from 'child_process';
  9. import path from 'path';
  10. import { fileURLToPath } from 'url';
  11. const __filename = fileURLToPath(import.meta.url);
  12. const __dirname = path.dirname(__filename);
  13. const workspaceRoot = path.dirname(__dirname);
  14. // Auto-generate cognio.md file for AI context
  15. import fs from 'fs';
  16. const cognioMdPath = path.join(workspaceRoot, 'cognio.md');
  17. const cognioContent = `# Cognio Memory System
  18. This workspace has access to **Cognio** - a semantic memory system via MCP.
  19. ## Available Tools (12 total)
  20. ### Core Memory Operations
  21. - **save_memory** - Save text with optional project/tags
  22. - **search_memory** - Semantic search across memories
  23. - **list_memories** - Browse all memories with filters
  24. - **get_memory** - Get full content of a specific memory by ID
  25. - **get_memory_stats** - Get statistics and insights
  26. - **archive_memory** - Soft delete a memory by ID
  27. - **delete_memory** - Permanently delete a memory
  28. - **export_memories** - Export to JSON or Markdown
  29. ### Project Context Management
  30. - **set_active_project** - Set active project (auto-applies to operations)
  31. - **get_active_project** - Check current active project
  32. - **list_projects** - See all available projects with memory counts
  33. ### Utilities
  34. - **summarize_text** - Summarize long text (extractive/abstractive)
  35. ## Usage
  36. Use these tools naturally when:
  37. - User asks about past work ("what did we do before?")
  38. - Solving problems worth remembering
  39. - Need to recall project-specific context
  40. ## Active Project System
  41. Set a project to auto-filter all operations:
  42. \`\`\`
  43. set_active_project("my-project")
  44. save_memory("solution here") // auto-saves to my-project
  45. search_memory("past solution") // auto-searches in my-project only
  46. \`\`\`
  47. Keep memories organized by project to avoid context mixing.
  48. `;
  49. try {
  50. fs.writeFileSync(cognioMdPath, cognioContent, 'utf8');
  51. } catch (error) {
  52. // Silent fail - not critical
  53. }
  54. // Auto-setup: Generate settings for all AI clients (silent mode)
  55. try {
  56. const setupScript = path.join(workspaceRoot, 'scripts', 'setup-clients.js');
  57. execSync(`node "${setupScript}"`, { stdio: 'pipe', cwd: workspaceRoot });
  58. // Silent - no console output to avoid terminal spam
  59. } catch (error) {
  60. // Silent error handling
  61. }
  62. // Cognio API base URL and API Key
  63. const COGNIO_API_URL = process.env.COGNIO_API_URL || "http://localhost:8080";
  64. const COGNIO_API_KEY = process.env.COGNIO_API_KEY;
  65. // Active project state (persists during MCP session)
  66. let activeProject = null;
  67. // Helper function to make API calls to Cognio
  68. async function cognioRequest(endpoint, method = "GET", body = null) {
  69. const url = `${COGNIO_API_URL}${endpoint}`;
  70. const options = {
  71. method,
  72. headers: {
  73. "Content-Type": "application/json",
  74. },
  75. };
  76. // Add API key if provided
  77. if (COGNIO_API_KEY) {
  78. options.headers["X-API-Key"] = COGNIO_API_KEY;
  79. }
  80. if (body) {
  81. options.body = JSON.stringify(body);
  82. }
  83. const response = await fetch(url, options);
  84. if (!response.ok) {
  85. const error = await response.text();
  86. throw new Error(`Cognio API error: ${response.status} - ${error}`);
  87. }
  88. return response.json();
  89. }
  90. // Create MCP server
  91. const server = new Server(
  92. {
  93. name: "cognio-mcp-server",
  94. version: "1.0.0",
  95. },
  96. {
  97. capabilities: {
  98. tools: {},
  99. },
  100. }
  101. );
  102. // List available tools
  103. server.setRequestHandler(ListToolsRequestSchema, async () => {
  104. return {
  105. tools: [
  106. {
  107. name: "save_memory",
  108. description: "Save information to long-term semantic memory with automatic tagging and categorization. Best practice: set an active project first with set_active_project to keep memories organized and avoid context mixing. If not using active project, you must provide a project parameter. Tags are optional — when auto-tagging is enabled and configured, tags will be generated automatically if not provided.",
  109. inputSchema: {
  110. type: "object",
  111. properties: {
  112. text: {
  113. type: "string",
  114. description: "The memory content to save",
  115. },
  116. project: {
  117. type: "string",
  118. description: "Project name to organize the memory (REQUIRED unless active project is set; RECOMMENDED: use current workspace/repo name or topic)",
  119. },
  120. tags: {
  121. type: "array",
  122. items: { type: "string" },
  123. description: "Optional tags for categorization (auto-generated if not provided)",
  124. },
  125. metadata: {
  126. type: "object",
  127. description: "Optional metadata as key-value pairs",
  128. },
  129. },
  130. required: ["text"],
  131. },
  132. },
  133. {
  134. name: "search_memory",
  135. description: "Search memories using semantic similarity. Use default (detailed=false) for quick exploration or when context is sufficient; it shows previews with IDs and saves input tokens. Use detailed=true only when you need the full text of results. For a specific item, use get_memory(id) after getting the ID from search. TIP: Filter by project to avoid mixing contexts from different workspaces.",
  136. inputSchema: {
  137. type: "object",
  138. properties: {
  139. query: {
  140. type: "string",
  141. description: "Search query text",
  142. },
  143. project: {
  144. type: "string",
  145. description: "Filter by project name (RECOMMENDED to avoid cross-project results)",
  146. },
  147. tags: {
  148. type: "array",
  149. items: { type: "string" },
  150. description: "Optional tags to filter by",
  151. },
  152. limit: {
  153. type: "number",
  154. description: "Maximum number of results (default: 5)",
  155. default: 5,
  156. },
  157. detailed: {
  158. type: "boolean",
  159. description: "Return full text (true) or truncated preview with IDs (false, default). Use default to save input tokens; set true only when full text is necessary.",
  160. default: false,
  161. },
  162. },
  163. required: ["query"],
  164. },
  165. },
  166. {
  167. name: "list_memories",
  168. description: "List all memories with optional filtering. Best practice: set an active project first to avoid mixing contexts from different workspaces. Use default (full_text=false) for quick browsing; set full_text=true only when you need complete content. For many items, consider using search_memory with relevant keywords instead.",
  169. inputSchema: {
  170. type: "object",
  171. properties: {
  172. project: {
  173. type: "string",
  174. description: "Filter by project name (REQUIRED unless active project is set; RECOMMENDED to avoid cross-project results)",
  175. },
  176. tags: {
  177. type: "array",
  178. items: { type: "string" },
  179. description: "Filter by tags",
  180. },
  181. page: {
  182. type: "number",
  183. description: "Page number (1-indexed)",
  184. default: 1,
  185. },
  186. limit: {
  187. type: "number",
  188. description: "Maximum number of results",
  189. default: 20,
  190. },
  191. offset: {
  192. type: "number",
  193. description: "Number of results to skip (legacy; will be converted to page)",
  194. default: 0,
  195. },
  196. full_text: {
  197. type: "boolean",
  198. description: "If true, return full text in output (no truncation). Use default to save input tokens; set true only when full text is necessary.",
  199. default: false,
  200. },
  201. },
  202. },
  203. },
  204. {
  205. name: "get_memory_stats",
  206. description: "Get statistics about stored memories",
  207. inputSchema: {
  208. type: "object",
  209. properties: {},
  210. },
  211. },
  212. {
  213. name: "archive_memory",
  214. description: "Archive (soft delete) a memory by ID",
  215. inputSchema: {
  216. type: "object",
  217. properties: {
  218. memory_id: {
  219. type: "string",
  220. description: "The ID of the memory to archive",
  221. },
  222. },
  223. required: ["memory_id"],
  224. },
  225. },
  226. {
  227. name: "summarize_text",
  228. description: "Summarize long text using extractive or abstractive methods",
  229. inputSchema: {
  230. type: "object",
  231. properties: {
  232. text: {
  233. type: "string",
  234. description: "The text to summarize",
  235. },
  236. num_sentences: {
  237. type: "number",
  238. description: "Number of sentences in summary (default: 3, max: 10)",
  239. },
  240. },
  241. required: ["text"],
  242. },
  243. },
  244. {
  245. name: "export_memories",
  246. description: "Export memories to JSON or Markdown format. Useful for backups or analyzing memory content.",
  247. inputSchema: {
  248. type: "object",
  249. properties: {
  250. format: {
  251. type: "string",
  252. description: "Export format: 'json' or 'markdown'",
  253. enum: ["json", "markdown"],
  254. default: "json",
  255. },
  256. project: {
  257. type: "string",
  258. description: "Optional: filter by project name",
  259. },
  260. },
  261. },
  262. },
  263. {
  264. name: "delete_memory",
  265. description: "Permanently delete a memory by ID. Use archive_memory instead for soft delete.",
  266. inputSchema: {
  267. type: "object",
  268. properties: {
  269. memory_id: {
  270. type: "string",
  271. description: "The ID of the memory to delete",
  272. },
  273. },
  274. required: ["memory_id"],
  275. },
  276. },
  277. {
  278. name: "set_active_project",
  279. description: "Set the active project context. All subsequent operations (save/search/list) will default to this project unless explicitly overridden. Like switching git branches - keeps you focused on one project at a time.",
  280. inputSchema: {
  281. type: "object",
  282. properties: {
  283. project: {
  284. type: "string",
  285. description: "Project name to activate (e.g., 'Helios-LoadBalancer', 'Cognio-Memory')",
  286. },
  287. },
  288. required: ["project"],
  289. },
  290. },
  291. {
  292. name: "get_active_project",
  293. description: "Get the currently active project context. Returns the project name that's currently active, or null if none set.",
  294. inputSchema: {
  295. type: "object",
  296. properties: {},
  297. },
  298. },
  299. {
  300. name: "list_projects",
  301. description: "List all available projects in the database. Useful for discovering projects before setting one as active.",
  302. inputSchema: {
  303. type: "object",
  304. properties: {},
  305. },
  306. },
  307. {
  308. name: "get_memory",
  309. description: "Get a single memory by ID to view its full content. Use this when you need to read the complete text of a specific memory.",
  310. inputSchema: {
  311. type: "object",
  312. properties: {
  313. memory_id: {
  314. type: "string",
  315. description: "The ID of the memory to retrieve",
  316. },
  317. },
  318. required: ["memory_id"],
  319. },
  320. },
  321. ],
  322. };
  323. });
  324. // Handle tool calls
  325. server.setRequestHandler(CallToolRequestSchema, async (request) => {
  326. const { name, arguments: args } = request.params;
  327. try {
  328. switch (name) {
  329. case "save_memory": {
  330. // Require either explicit project or active project
  331. if (!args.project && !activeProject) {
  332. return {
  333. content: [
  334. {
  335. type: "text",
  336. text: "ERROR: Must set active project first or specify project parameter.\n\nUse set_active_project(\"project-name\") to enable memory operations.",
  337. },
  338. ],
  339. isError: true,
  340. };
  341. }
  342. console.error(`[Cognio] Saving memory to project: ${args.project || activeProject}`);
  343. const result = await cognioRequest("/memory/save", "POST", {
  344. text: args.text,
  345. project: args.project || activeProject,
  346. tags: args.tags,
  347. metadata: args.metadata,
  348. });
  349. console.error(`[Cognio] Memory saved successfully - ID: ${result.id}`);
  350. let message = JSON.stringify(result, null, 2);
  351. if (!args.project && activeProject) {
  352. message += `\n\n[INFO] Auto-saved to active project: ${activeProject}`;
  353. }
  354. return {
  355. content: [
  356. {
  357. type: "text",
  358. text: message,
  359. },
  360. ],
  361. };
  362. }
  363. case "search_memory": {
  364. // Require active project for search
  365. if (!activeProject && !args.project) {
  366. return {
  367. content: [
  368. {
  369. type: "text",
  370. text: "ERROR: Must set active project first or specify project parameter.\n\nUse set_active_project(\"project-name\") to search memories.",
  371. },
  372. ],
  373. isError: true,
  374. };
  375. }
  376. const projectToUse = args.project || activeProject;
  377. const detailed = args.detailed === true;
  378. console.error(`[Cognio] Searching memories - query: "${args.query}", project: ${projectToUse}, detailed: ${detailed}`);
  379. const params = new URLSearchParams({
  380. q: args.query,
  381. limit: String(args.limit || 5),
  382. });
  383. if (projectToUse) params.append("project", projectToUse);
  384. if (args.tags && args.tags.length > 0) {
  385. params.append("tags", args.tags.join(","));
  386. }
  387. const result = await cognioRequest(`/memory/search?${params}`);
  388. console.error(`[Cognio] Search completed - found ${result.total} results`);
  389. // Format results nicely
  390. let response = projectToUse && !args.project
  391. ? `[Active Project: ${projectToUse}]\nFound ${result.total} memories:\n\n`
  392. : `Found ${result.total} memories:\n\n`;
  393. result.results.forEach((mem, idx) => {
  394. const scoreText = typeof mem.score === 'number' ? mem.score.toFixed(3) : 'N/A';
  395. response += `${idx + 1}. [Score: ${scoreText}] [ID: ${mem.id}]\n`;
  396. // Truncate text if not detailed
  397. const preview = detailed ? mem.text : `${mem.text.substring(0, 120)}${mem.text.length > 120 ? '…' : ''}`;
  398. response += ` Text: ${preview}\n`;
  399. if (mem.project) response += ` Project: ${mem.project}\n`;
  400. if (mem.tags && mem.tags.length > 0) {
  401. response += ` Tags: ${mem.tags.join(", ")}\n`;
  402. }
  403. response += ` Created: ${mem.created_at}\n`;
  404. if (!detailed) {
  405. response += ` (Use get_memory("${mem.id}") for full text)\n`;
  406. }
  407. response += `\n`;
  408. });
  409. if (!detailed) {
  410. response += `Tip: Use detailed=true for full text or get_memory(id) for specific items.`;
  411. }
  412. return {
  413. content: [
  414. {
  415. type: "text",
  416. text: response,
  417. },
  418. ],
  419. };
  420. }
  421. case "list_memories": {
  422. // Require active project for list
  423. if (!activeProject && !args.project) {
  424. return {
  425. content: [
  426. {
  427. type: "text",
  428. text: "ERROR: Must set active project first or specify project parameter.\n\nUse set_active_project(\"project-name\") to list memories.",
  429. },
  430. ],
  431. isError: true,
  432. };
  433. }
  434. const projectToUse = args.project || activeProject;
  435. const limit = Number.isFinite(args?.limit) ? Number(args.limit) : 20;
  436. const page = Number.isFinite(args?.page)
  437. ? Number(args.page)
  438. : (Number.isFinite(args?.offset) ? Math.floor(Number(args.offset) / limit) + 1 : 1);
  439. const params = new URLSearchParams({
  440. limit: String(limit),
  441. page: String(page),
  442. sort: "date",
  443. });
  444. if (projectToUse) params.append("project", projectToUse);
  445. if (args.tags && args.tags.length > 0) {
  446. params.append("tags", args.tags.join(","));
  447. }
  448. const result = await cognioRequest(`/memory/list?${params}`);
  449. const total = (typeof result.total_items === 'number') ? result.total_items : (result.total ?? 0);
  450. let response = projectToUse && !args.project
  451. ? `[Active Project: ${projectToUse}]\nTotal: ${total} memories (page ${result.page} of ${result.total_pages}, showing ${result.memories.length})\n\n`
  452. : `Total: ${total} memories (page ${result.page} of ${result.total_pages}, showing ${result.memories.length})\n\n`;
  453. result.memories.forEach((mem, idx) => {
  454. const text = args.full_text ? mem.text : `${mem.text.substring(0, 100)}${mem.text.length > 100 ? '...' : ''}`;
  455. response += `${idx + 1}. ${text}\n`;
  456. if (mem.project) response += ` Project: ${mem.project}\n`;
  457. if (mem.tags && mem.tags.length > 0) {
  458. response += ` Tags: ${mem.tags.join(", ")}\n`;
  459. }
  460. response += ` ID: ${mem.id}\n\n`;
  461. });
  462. return {
  463. content: [
  464. {
  465. type: "text",
  466. text: response,
  467. },
  468. ],
  469. };
  470. }
  471. case "get_memory_stats": {
  472. const result = await cognioRequest("/memory/stats");
  473. return {
  474. content: [
  475. {
  476. type: "text",
  477. text: JSON.stringify(result, null, 2),
  478. },
  479. ],
  480. };
  481. }
  482. case "archive_memory": {
  483. const result = await cognioRequest(
  484. `/memory/${args.memory_id}/archive`,
  485. "POST"
  486. );
  487. return {
  488. content: [
  489. {
  490. type: "text",
  491. text: JSON.stringify(result, null, 2),
  492. },
  493. ],
  494. };
  495. }
  496. case "summarize_text": {
  497. const result = await cognioRequest("/memory/summarize", "POST", {
  498. text: args.text,
  499. num_sentences: args.num_sentences || 3,
  500. });
  501. const response = `Summary:\n${result.summary}\n\nStats:\n- Original: ${result.original_length} words\n- Summary: ${result.summary_length} words\n- Method: ${result.method}`;
  502. return {
  503. content: [
  504. {
  505. type: "text",
  506. text: response,
  507. },
  508. ],
  509. };
  510. }
  511. case "export_memories": {
  512. const format = args.format || "json";
  513. const params = new URLSearchParams();
  514. if (args.project) params.append("project", args.project);
  515. params.append("format", format);
  516. const url = `${COGNIO_API_URL}/memory/export?${params}`;
  517. const options = {
  518. method: "GET",
  519. headers: {},
  520. };
  521. // Add API key if provided
  522. if (COGNIO_API_KEY) {
  523. options.headers["X-API-Key"] = COGNIO_API_KEY;
  524. }
  525. const response = await fetch(url, options);
  526. if (!response.ok) {
  527. throw new Error(`Export failed: ${response.statusText}`);
  528. }
  529. const exportData = await response.text();
  530. return {
  531. content: [
  532. {
  533. type: "text",
  534. text: `[EXPORT] Format: ${format}\n${args.project ? `Project: ${args.project}\n` : ''}\n${exportData}`,
  535. },
  536. ],
  537. };
  538. }
  539. case "delete_memory": {
  540. const result = await cognioRequest(`/memory/${args.memory_id}`, "DELETE");
  541. return {
  542. content: [
  543. {
  544. type: "text",
  545. text: `[OK] Memory ${args.memory_id} permanently deleted`,
  546. },
  547. ],
  548. };
  549. }
  550. case "set_active_project": {
  551. activeProject = args.project;
  552. return {
  553. content: [
  554. {
  555. type: "text",
  556. text: `[OK] Active project set to: ${activeProject}\n\nAll save/search/list operations will now default to this project unless you specify a different one.`,
  557. },
  558. ],
  559. };
  560. }
  561. case "get_active_project": {
  562. return {
  563. content: [
  564. {
  565. type: "text",
  566. text: activeProject
  567. ? `Current active project: ${activeProject}`
  568. : `No active project set. Use set_active_project to activate one.`,
  569. },
  570. ],
  571. };
  572. }
  573. case "list_projects": {
  574. const stats = await cognioRequest("/memory/stats");
  575. const projectsObj = stats.memories_by_project || {};
  576. const entries = Object.entries(projectsObj);
  577. const list = entries
  578. .sort((a, b) => b[1] - a[1])
  579. .map(([name, count]) => ({ name, count }));
  580. return {
  581. content: [
  582. {
  583. type: "text",
  584. text: JSON.stringify(list),
  585. },
  586. ],
  587. };
  588. }
  589. case "get_memory": {
  590. console.error(`[Cognio] Retrieving memory by ID: ${args.memory_id}`);
  591. const result = await cognioRequest(`/memory/${args.memory_id}`);
  592. let response = `Memory Details:\n\n`;
  593. response += `ID: ${result.id}\n`;
  594. response += `Text: ${result.text}\n`;
  595. if (result.project) response += `Project: ${result.project}\n`;
  596. if (result.tags && result.tags.length > 0) {
  597. response += `Tags: ${result.tags.join(", ")}\n`;
  598. }
  599. response += `Created: ${new Date(result.created_at * 1000).toISOString()}\n`;
  600. response += `Updated: ${new Date(result.updated_at * 1000).toISOString()}\n`;
  601. return {
  602. content: [
  603. {
  604. type: "text",
  605. text: response,
  606. },
  607. ],
  608. };
  609. }
  610. default:
  611. throw new Error(`Unknown tool: ${name}`);
  612. }
  613. } catch (error) {
  614. return {
  615. content: [
  616. {
  617. type: "text",
  618. text: `Error: ${error.message}`,
  619. },
  620. ],
  621. isError: true,
  622. };
  623. }
  624. });
  625. // Start server
  626. async function main() {
  627. const transport = new StdioServerTransport();
  628. await server.connect(transport);
  629. // Silent mode - no console output
  630. }
  631. main().catch((error) => {
  632. // Silent error handling
  633. process.exit(1);
  634. });