@mentagen/mcp 0.8.0 → 0.8.2

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,197 @@
1
+ import { randomUUID } from 'crypto';
2
+ /**
3
+ * Lightweight HTTP client for Mentagen's Bifrost API.
4
+ *
5
+ * Uses the HTTP transport endpoint (/__h) to call server methods.
6
+ * Bifrost expects text/plain with a specific payload/context structure.
7
+ */
8
+ export class MentagenClient {
9
+ baseUrl;
10
+ token;
11
+ constructor(config) {
12
+ this.baseUrl = config.mentagenUrl.replace(/\/$/, '');
13
+ this.token = config.token;
14
+ }
15
+ /**
16
+ * Call a Bifrost method via HTTP.
17
+ *
18
+ * Bifrost's HTTP transport expects:
19
+ * - Content-Type: text/plain
20
+ * - Body: JSON with { payload: { uuid, method, params }, context: { token } }
21
+ */
22
+ async call(method, params = {}) {
23
+ const url = `${this.baseUrl}/__h`;
24
+ const body = JSON.stringify({
25
+ payload: {
26
+ uuid: randomUUID(),
27
+ method,
28
+ params,
29
+ },
30
+ context: {
31
+ token: this.token,
32
+ },
33
+ });
34
+ const response = await fetch(url, {
35
+ method: 'POST',
36
+ headers: {
37
+ 'Content-Type': 'text/plain',
38
+ },
39
+ body,
40
+ });
41
+ if (!response.ok) {
42
+ const text = await response.text();
43
+ throw new Error(`HTTP ${response.status}: ${text}`);
44
+ }
45
+ const data = (await response.json());
46
+ if (data.type === 'error' || data.error) {
47
+ const errorMsg = data.message ||
48
+ (typeof data.error === 'string' ? data.error : data.error?.message);
49
+ throw new Error(errorMsg || 'Unknown error');
50
+ }
51
+ return data.result;
52
+ }
53
+ /**
54
+ * Verify the token and get the current user.
55
+ */
56
+ async getCurrentUser() {
57
+ try {
58
+ return await this.call('user.current');
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ /**
65
+ * List boards the user has access to.
66
+ */
67
+ async listBoards(params) {
68
+ return this.call('boards.listSimple', {
69
+ orgId: params?.orgId,
70
+ });
71
+ }
72
+ /**
73
+ * Search boards by name.
74
+ */
75
+ async searchBoards(query, limit = 20) {
76
+ const result = await this.call('boards.search', {
77
+ query,
78
+ limit,
79
+ });
80
+ return result.data || [];
81
+ }
82
+ /**
83
+ * Search nodes using hybrid semantic/keyword search.
84
+ */
85
+ async searchNodes(params) {
86
+ return this.call('boards.searchNodesText', params);
87
+ }
88
+ /**
89
+ * List organizations the user has access to.
90
+ */
91
+ async listOrganizations() {
92
+ return this.call('org.list', {});
93
+ }
94
+ /**
95
+ * List teams within an organization.
96
+ */
97
+ async listTeams(params) {
98
+ return this.call('team.list', params);
99
+ }
100
+ /**
101
+ * Add a new node to a board.
102
+ */
103
+ async addNode(params) {
104
+ return this.call('boards.addNode', params);
105
+ }
106
+ /**
107
+ * Update a node's properties.
108
+ */
109
+ async updateNode(params) {
110
+ return this.call('boards.updateNode', params);
111
+ }
112
+ /**
113
+ * Get a single node by ID.
114
+ * Uses getNodeForMcp which excludes large fields (chat, textDetections, _vectors)
115
+ * in favor of their computed alternatives (chatConcatenated, textDetectionsConcatenated).
116
+ */
117
+ async getNode(params) {
118
+ return this.call('boards.getNodeForMcp', params);
119
+ }
120
+ /**
121
+ * List all nodes in a board (basic fields only).
122
+ */
123
+ async listNodes(params) {
124
+ return this.call('boards.listNodesSimple', params);
125
+ }
126
+ /**
127
+ * List all nodes with full content fields for extraction.
128
+ * Includes: content, code, chatConcatenated, textDetectionsConcatenated, pdfText, article
129
+ * Note: Uses computed fields instead of raw chat/textDetections.
130
+ */
131
+ async listNodesWithContent(params) {
132
+ return this.call('boards.listNodesWithContent', params);
133
+ }
134
+ /**
135
+ * Soft-delete a node.
136
+ */
137
+ async deleteNode(params) {
138
+ await this.call('boards.deleteNode', params);
139
+ }
140
+ /**
141
+ * Create a new board.
142
+ */
143
+ async createBoard(params) {
144
+ return this.call('boards.create', params);
145
+ }
146
+ /**
147
+ * Get a single board by ID.
148
+ */
149
+ async getBoard(params) {
150
+ return this.call('boards.get', params);
151
+ }
152
+ /**
153
+ * Update a board's properties.
154
+ */
155
+ async updateBoard(params) {
156
+ return this.call('boards.update', params);
157
+ }
158
+ /**
159
+ * Soft-delete a board.
160
+ */
161
+ async deleteBoard(params) {
162
+ await this.call('boards.deleteBoard', params);
163
+ }
164
+ /**
165
+ * Add an edge between two nodes.
166
+ */
167
+ async addEdge(params) {
168
+ return this.call('boards.addEdge', params);
169
+ }
170
+ /**
171
+ * List all edges in a board.
172
+ */
173
+ async listEdges(params) {
174
+ return this.call('boards.listEdgesSimple', params);
175
+ }
176
+ /**
177
+ * Update an edge's metadata (direction and/or label).
178
+ */
179
+ async updateEdge(params) {
180
+ return this.call('boards.updateEdgeMetadata', params);
181
+ }
182
+ /**
183
+ * Soft-delete an edge.
184
+ */
185
+ async deleteEdge(params) {
186
+ await this.call('boards.deleteEdge', params);
187
+ }
188
+ }
189
+ export async function createMentagenClient(config) {
190
+ const client = new MentagenClient(config);
191
+ const user = await client.getCurrentUser();
192
+ if (!user) {
193
+ throw new Error('Authentication failed. Please check your MENTAGEN_TOKEN.\n' +
194
+ 'Generate a new token in Mentagen Settings > Developer if needed.');
195
+ }
196
+ return client;
197
+ }
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { createMentagenClient } from './client/helene.js';
4
+ import { createMentagenClient } from './client/bifrost.js';
5
5
  import { registerPrompts } from './prompts/index.js';
6
6
  import { registerResources } from './resources/index.js';
7
7
  import { registerTools } from './tools/index.js';
@@ -41,6 +41,16 @@ function stripHtml(html) {
41
41
  .replace(/'/g, "'")
42
42
  .trim();
43
43
  }
44
+ /**
45
+ * Format todos as markdown checkboxes.
46
+ */
47
+ function formatTodos(todos) {
48
+ if (!todos || todos.length === 0)
49
+ return '';
50
+ const sorted = [...todos].sort((a, b) => a.order - b.order);
51
+ const lines = sorted.map(t => `- [${t.completed ? 'x' : ' '}] ${t.text}`);
52
+ return lines.join('\n');
53
+ }
44
54
  /**
45
55
  * Infer programming language from file extension.
46
56
  */
@@ -121,6 +131,7 @@ function formatContent(node) {
121
131
  */
122
132
  function formatNodeSection(node, baseUrl, boardId) {
123
133
  const content = formatContent(node);
134
+ const todos = formatTodos(node.todos);
124
135
  const link = getNodeUrl(baseUrl, boardId, node._id);
125
136
  const lines = [
126
137
  `## ${node.name}`,
@@ -128,6 +139,12 @@ function formatNodeSection(node, baseUrl, boardId) {
128
139
  '',
129
140
  content,
130
141
  ];
142
+ // Add todos section if present
143
+ if (todos) {
144
+ if (content)
145
+ lines.push('', '### Todos', '');
146
+ lines.push(todos);
147
+ }
131
148
  return lines.join('\n');
132
149
  }
133
150
  export async function handleExtractBoardContent(client, params, baseUrl) {
@@ -150,12 +167,13 @@ export async function handleExtractBoardContent(client, params, baseUrl) {
150
167
  ],
151
168
  };
152
169
  }
153
- // Filter nodes based on content
170
+ // Filter nodes based on content or todos
154
171
  const nodesWithContent = params.includeEmpty
155
172
  ? nodes
156
173
  : nodes.filter(n => {
157
174
  const content = getNodeContent(n);
158
- return content && content.trim().length > 0;
175
+ const hasTodos = n.todos && n.todos.length > 0;
176
+ return (content && content.trim().length > 0) || hasTodos;
159
177
  });
160
178
  if (nodesWithContent.length === 0) {
161
179
  return {
@@ -23,6 +23,13 @@ function truncateContent(content, maxLength) {
23
23
  return ((lastSpace > maxLength * 0.7 ? truncated.slice(0, lastSpace) : truncated) +
24
24
  '...');
25
25
  }
26
+ /** Format todos as "completed/total" or empty string if no todos. */
27
+ function formatTodoCounts(todos) {
28
+ if (!todos || todos.length === 0)
29
+ return '';
30
+ const completed = todos.filter(t => t.completed).length;
31
+ return `${completed}/${todos.length}`;
32
+ }
26
33
  function formatNodesTsv(nodes) {
27
34
  const rows = nodes.map(n => ({
28
35
  id: n._id,
@@ -33,6 +40,7 @@ function formatNodesTsv(nodes) {
33
40
  width: pixelsToUnits(n.width),
34
41
  height: pixelsToUnits(n.height),
35
42
  content: truncateContent(n.content ?? null, MAX_CONTENT_LENGTH),
43
+ todos: formatTodoCounts(n.todos),
36
44
  updatedAt: formatDate(n.updatedAt),
37
45
  link: n.link,
38
46
  }));
@@ -49,6 +57,7 @@ function formatNodesTsv(nodes) {
49
57
  'width',
50
58
  'height',
51
59
  'content',
60
+ 'todos',
52
61
  'updatedAt',
53
62
  'link',
54
63
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentagen/mcp",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "MCP server for Mentagen knowledge base integration with Cursor",
5
5
  "type": "module",
6
6
  "bin": "dist/index.js",