@mentagen/mcp 0.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.
Files changed (39) hide show
  1. package/README.md +100 -0
  2. package/dist/client/helene.js +178 -0
  3. package/dist/client/types.js +27 -0
  4. package/dist/index.js +26 -0
  5. package/dist/tools/batch-update.js +118 -0
  6. package/dist/tools/boards.js +50 -0
  7. package/dist/tools/check-collision.js +55 -0
  8. package/dist/tools/colors.js +35 -0
  9. package/dist/tools/create-board.js +50 -0
  10. package/dist/tools/create-edge.js +61 -0
  11. package/dist/tools/create-graph.js +174 -0
  12. package/dist/tools/create.js +152 -0
  13. package/dist/tools/delete-edge.js +36 -0
  14. package/dist/tools/delete.js +36 -0
  15. package/dist/tools/extract-board-content.js +207 -0
  16. package/dist/tools/find-position.js +117 -0
  17. package/dist/tools/grid-calc.js +205 -0
  18. package/dist/tools/index.js +940 -0
  19. package/dist/tools/link.js +22 -0
  20. package/dist/tools/list-edges.js +58 -0
  21. package/dist/tools/list-nodes.js +96 -0
  22. package/dist/tools/list-positions.js +64 -0
  23. package/dist/tools/node-types.js +65 -0
  24. package/dist/tools/patch-content.js +143 -0
  25. package/dist/tools/read.js +41 -0
  26. package/dist/tools/search-board.js +99 -0
  27. package/dist/tools/search-boards.js +50 -0
  28. package/dist/tools/search.js +89 -0
  29. package/dist/tools/size-calc.js +108 -0
  30. package/dist/tools/update-board.js +64 -0
  31. package/dist/tools/update-edge.js +63 -0
  32. package/dist/tools/update.js +157 -0
  33. package/dist/utils/collision.js +177 -0
  34. package/dist/utils/config.js +16 -0
  35. package/dist/utils/ejson.js +30 -0
  36. package/dist/utils/errors.js +16 -0
  37. package/dist/utils/formatting.js +15 -0
  38. package/dist/utils/urls.js +18 -0
  39. package/package.json +49 -0
package/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # Mentagen MCP Server
2
+
3
+ > **⚠️ Preview Access Only**
4
+ >
5
+ > This MCP server is currently in preview and available exclusively to select premium users. If you don't have access yet, please contact support or wait for the general availability release.
6
+
7
+ MCP (Model Context Protocol) server that enables AI agents in Cursor to interact with your Mentagen knowledge base.
8
+
9
+ ## Overview
10
+
11
+ This MCP server acts as a bridge between Cursor's AI agent and Mentagen's backend. Once configured, you can ask Cursor's AI to search, create, update, and manage content in your knowledge base using natural language.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ # Global install
17
+ npm install -g @mentagen/mcp
18
+
19
+ # Or use npx (no install required)
20
+ npx @mentagen/mcp
21
+ ```
22
+
23
+ ## Configuration
24
+
25
+ ### 1. Generate a Development Token
26
+
27
+ 1. Open Mentagen and navigate to **Settings > Developer**
28
+ 2. Click **Generate Token**
29
+ 3. Give your token a descriptive name (e.g., "Cursor MCP")
30
+ 4. Copy the generated token (starts with `sd:`)
31
+
32
+ > **Important**: The token is only shown once. Store it securely.
33
+
34
+ ### 2. Configure Cursor
35
+
36
+ Add the MCP server to your Cursor configuration:
37
+
38
+ **macOS/Linux**: `~/.cursor/mcp.json`
39
+ **Windows**: `%USERPROFILE%\.cursor\mcp.json`
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "mentagen": {
45
+ "command": "npx",
46
+ "args": ["@mentagen/mcp"],
47
+ "env": {
48
+ "MENTAGEN_TOKEN": "sd:your-development-token-here",
49
+ "MENTAGEN_URL": "https://mentagen.com"
50
+ }
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ ### 3. Restart Cursor
57
+
58
+ After saving the configuration, restart Cursor for the changes to take effect.
59
+
60
+ ## Environment Variables
61
+
62
+ | Variable | Required | Default | Description |
63
+ | ---------------- | -------- | ---------------------- | ------------------------------------------ |
64
+ | `MENTAGEN_TOKEN` | Yes | - | Your development token (starts with `sd:`) |
65
+ | `MENTAGEN_URL` | No | `https://mentagen.com` | Mentagen server URL |
66
+
67
+ ## What You Can Do
68
+
69
+ Once configured, ask Cursor's AI to help you with:
70
+
71
+ - **Search** — Find nodes across your boards using semantic or keyword search
72
+ - **Read & List** — View board contents, node details, and connections
73
+ - **Create** — Add new boards, nodes, and edges
74
+ - **Update** — Modify node content, positions, colors, and connections
75
+ - **Organize** — Batch update multiple items, create node graphs, find non-colliding positions
76
+
77
+ The AI automatically discovers available tools and their parameters — just describe what you want to accomplish.
78
+
79
+ ## Troubleshooting
80
+
81
+ ### "Authentication failed" Error
82
+
83
+ - Verify your token is correct and starts with `sd:`
84
+ - Check that the token hasn't been revoked in Settings > Developer
85
+ - Ensure `MENTAGEN_URL` points to the correct server
86
+
87
+ ### Tools Not Appearing in Cursor
88
+
89
+ - Restart Cursor after updating `mcp.json`
90
+ - Check Cursor's MCP logs for errors (Help > Toggle Developer Tools)
91
+ - Verify the JSON syntax in `mcp.json` is valid
92
+
93
+ ### "Permission denied" Errors
94
+
95
+ - Your token may not have access to the requested board
96
+ - Verify you have the correct permissions in Mentagen
97
+
98
+ ## License
99
+
100
+ Proprietary. All rights reserved.
@@ -0,0 +1,178 @@
1
+ import { randomUUID } from 'crypto';
2
+ /**
3
+ * Lightweight HTTP client for Mentagen's Helene API.
4
+ *
5
+ * Uses the HTTP transport endpoint (/__h) to call server methods.
6
+ * Helene 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 Helene method via HTTP.
17
+ *
18
+ * Helene'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() {
68
+ const result = await this.call('boards.list', {});
69
+ return result.data || [];
70
+ }
71
+ /**
72
+ * Search boards by name.
73
+ */
74
+ async searchBoards(query, limit = 20) {
75
+ const result = await this.call('boards.search', {
76
+ query,
77
+ limit,
78
+ });
79
+ return result.data || [];
80
+ }
81
+ /**
82
+ * Search nodes using hybrid semantic/keyword search.
83
+ */
84
+ async searchNodes(params) {
85
+ return this.call('boards.searchNodesText', params);
86
+ }
87
+ /**
88
+ * Add a new node to a board.
89
+ */
90
+ async addNode(params) {
91
+ return this.call('boards.addNode', params);
92
+ }
93
+ /**
94
+ * Update a node's properties.
95
+ */
96
+ async updateNode(params) {
97
+ return this.call('boards.updateNode', params);
98
+ }
99
+ /**
100
+ * Get a single node by ID.
101
+ * Uses getNodeForMcp which excludes large fields (chat, textDetections, _vectors)
102
+ * in favor of their computed alternatives (chatConcatenated, textDetectionsConcatenated).
103
+ */
104
+ async getNode(params) {
105
+ return this.call('boards.getNodeForMcp', params);
106
+ }
107
+ /**
108
+ * List all nodes in a board (basic fields only).
109
+ */
110
+ async listNodes(params) {
111
+ return this.call('boards.listNodesSimple', params);
112
+ }
113
+ /**
114
+ * List all nodes with full content fields for extraction.
115
+ * Includes: content, code, chatConcatenated, textDetectionsConcatenated, pdfText, article
116
+ * Note: Uses computed fields instead of raw chat/textDetections.
117
+ */
118
+ async listNodesWithContent(params) {
119
+ return this.call('boards.listNodesWithContent', params);
120
+ }
121
+ /**
122
+ * Soft-delete a node.
123
+ */
124
+ async deleteNode(params) {
125
+ await this.call('boards.deleteNode', params);
126
+ }
127
+ /**
128
+ * Create a new board.
129
+ */
130
+ async createBoard(params) {
131
+ return this.call('boards.create', params);
132
+ }
133
+ /**
134
+ * Get a single board by ID.
135
+ */
136
+ async getBoard(params) {
137
+ return this.call('boards.get', params);
138
+ }
139
+ /**
140
+ * Update a board's properties.
141
+ */
142
+ async updateBoard(params) {
143
+ return this.call('boards.update', params);
144
+ }
145
+ /**
146
+ * Add an edge between two nodes.
147
+ */
148
+ async addEdge(params) {
149
+ return this.call('boards.addEdge', params);
150
+ }
151
+ /**
152
+ * List all edges in a board.
153
+ */
154
+ async listEdges(params) {
155
+ return this.call('boards.listEdgesSimple', params);
156
+ }
157
+ /**
158
+ * Update an edge's metadata (direction and/or label).
159
+ */
160
+ async updateEdge(params) {
161
+ return this.call('boards.updateEdgeMetadata', params);
162
+ }
163
+ /**
164
+ * Soft-delete an edge.
165
+ */
166
+ async deleteEdge(params) {
167
+ await this.call('boards.deleteEdge', params);
168
+ }
169
+ }
170
+ export async function createMentagenClient(config) {
171
+ const client = new MentagenClient(config);
172
+ const user = await client.getCurrentUser();
173
+ if (!user) {
174
+ throw new Error('Authentication failed. Please check your MENTAGEN_TOKEN.\n' +
175
+ 'Generate a new token in Mentagen Settings > Developer if needed.');
176
+ }
177
+ return client;
178
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Types for Mentagen API responses.
3
+ * These mirror the server-side types but are standalone for the MCP package.
4
+ */
5
+ export const EdgeDirection = {
6
+ None: 'none',
7
+ Source: 'source',
8
+ Target: 'target',
9
+ };
10
+ export const NodeType = {
11
+ Text: 'text',
12
+ Markdown: 'markdown',
13
+ Code: 'code',
14
+ Image: 'image',
15
+ PDF: 'pdf',
16
+ URL: 'url',
17
+ Voice: 'voice',
18
+ Contact: 'contact',
19
+ Board: 'board',
20
+ Audio: 'audio',
21
+ Mermaid: 'mermaid',
22
+ AIChat: 'ai-chat',
23
+ AIQuiz: 'ai-quiz',
24
+ File: 'file',
25
+ Excalidraw: 'excalidraw',
26
+ Teleprompter: 'teleprompter',
27
+ };
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { createMentagenClient } from './client/helene.js';
5
+ import { registerTools } from './tools/index.js';
6
+ import { loadConfig } from './utils/config.js';
7
+ async function main() {
8
+ const config = loadConfig();
9
+ const client = await createMentagenClient(config);
10
+ const mcpServer = new McpServer({
11
+ name: 'mentagen',
12
+ version: '0.1.0',
13
+ }, {
14
+ capabilities: {
15
+ tools: {},
16
+ },
17
+ });
18
+ registerTools(mcpServer.server, client, config);
19
+ const transport = new StdioServerTransport();
20
+ await mcpServer.connect(transport);
21
+ process.stderr.write('Mentagen MCP server started\n');
22
+ }
23
+ main().catch(error => {
24
+ process.stderr.write(`Error: ${error.message}\n`);
25
+ process.exit(1);
26
+ });
@@ -0,0 +1,118 @@
1
+ import { z } from 'zod';
2
+ import { formatError } from '../utils/errors.js';
3
+ const nodeUpdateSchema = z.object({
4
+ nodeId: z.string().describe('The node ID to update'),
5
+ name: z.string().optional().describe('New name for the node'),
6
+ content: z.string().optional().describe('New content for the node'),
7
+ color: z.string().optional().describe('Node color'),
8
+ x: z.number().optional().describe('X position'),
9
+ y: z.number().optional().describe('Y position'),
10
+ width: z.number().optional().describe('Node width'),
11
+ height: z.number().optional().describe('Node height'),
12
+ });
13
+ const edgeUpdateSchema = z.object({
14
+ edgeId: z.string().describe('The edge ID to update'),
15
+ direction: z
16
+ .enum(['none', 'source', 'target'])
17
+ .optional()
18
+ .describe('Arrow direction'),
19
+ label: z.string().optional().describe('Edge label'),
20
+ });
21
+ export const batchUpdateSchema = z.object({
22
+ boardId: z.string().describe('The board ID containing the items to update'),
23
+ nodes: z.array(nodeUpdateSchema).optional().describe('Array of node updates'),
24
+ edges: z.array(edgeUpdateSchema).optional().describe('Array of edge updates'),
25
+ });
26
+ export async function handleBatchUpdate(client, params) {
27
+ const nodeUpdates = params.nodes ?? [];
28
+ const edgeUpdates = params.edges ?? [];
29
+ if (nodeUpdates.length === 0 && edgeUpdates.length === 0) {
30
+ return {
31
+ content: [
32
+ {
33
+ type: 'text',
34
+ text: 'No updates provided. Include at least one node or edge update.',
35
+ },
36
+ ],
37
+ isError: true,
38
+ };
39
+ }
40
+ const results = {
41
+ nodes: [],
42
+ edges: [],
43
+ };
44
+ // Process node updates in parallel
45
+ const nodePromises = nodeUpdates.map(async (update) => {
46
+ const { nodeId, ...data } = update;
47
+ // Filter out undefined values
48
+ const cleanData = Object.fromEntries(Object.entries(data).filter(([, v]) => v !== undefined));
49
+ if (Object.keys(cleanData).length === 0) {
50
+ return { id: nodeId, success: false, error: 'No fields to update' };
51
+ }
52
+ try {
53
+ await client.updateNode({
54
+ boardId: params.boardId,
55
+ nodeId,
56
+ data: cleanData,
57
+ });
58
+ return { id: nodeId, success: true };
59
+ }
60
+ catch (error) {
61
+ return { id: nodeId, success: false, error: formatError(error) };
62
+ }
63
+ });
64
+ // Process edge updates in parallel
65
+ const edgePromises = edgeUpdates.map(async (update) => {
66
+ const { edgeId, direction, label } = update;
67
+ if (direction === undefined && label === undefined) {
68
+ return { id: edgeId, success: false, error: 'No fields to update' };
69
+ }
70
+ try {
71
+ await client.updateEdge({
72
+ boardId: params.boardId,
73
+ edgeId,
74
+ direction,
75
+ label,
76
+ });
77
+ return { id: edgeId, success: true };
78
+ }
79
+ catch (error) {
80
+ return { id: edgeId, success: false, error: formatError(error) };
81
+ }
82
+ });
83
+ // Wait for all updates to complete
84
+ results.nodes = await Promise.all(nodePromises);
85
+ results.edges = await Promise.all(edgePromises);
86
+ const nodeSuccessCount = results.nodes.filter(r => r.success).length;
87
+ const edgeSuccessCount = results.edges.filter(r => r.success).length;
88
+ const nodeFailCount = results.nodes.length - nodeSuccessCount;
89
+ const edgeFailCount = results.edges.length - edgeSuccessCount;
90
+ const errors = [
91
+ ...results.nodes.filter(r => !r.success),
92
+ ...results.edges.filter(r => !r.success),
93
+ ];
94
+ const response = {
95
+ success: errors.length === 0,
96
+ updated: {
97
+ nodes: nodeSuccessCount,
98
+ edges: edgeSuccessCount,
99
+ },
100
+ };
101
+ if (errors.length > 0) {
102
+ response.failed = {
103
+ nodes: nodeFailCount,
104
+ edges: edgeFailCount,
105
+ };
106
+ response.errors = errors.map(e => ({ id: e.id, error: e.error }));
107
+ }
108
+ const allFailed = errors.length === results.nodes.length + results.edges.length;
109
+ return {
110
+ content: [
111
+ {
112
+ type: 'text',
113
+ text: JSON.stringify(response, null, 2),
114
+ },
115
+ ],
116
+ ...(allFailed && { isError: true }),
117
+ };
118
+ }
@@ -0,0 +1,50 @@
1
+ import Papa from 'papaparse';
2
+ import { z } from 'zod';
3
+ import { formatShortDate } from '../utils/ejson.js';
4
+ import { formatError } from '../utils/errors.js';
5
+ import { getBoardUrl } from '../utils/urls.js';
6
+ export const boardsSchema = z.object({});
7
+ /**
8
+ * Format boards as TSV for compact output.
9
+ * Uses papaparse for proper escaping of special characters.
10
+ */
11
+ function formatBoardsTsv(boards) {
12
+ return Papa.unparse(boards, {
13
+ delimiter: '\t',
14
+ header: true,
15
+ newline: '\n',
16
+ columns: ['id', 'name', 'description', 'nodeCount', 'updatedAt', 'link'],
17
+ });
18
+ }
19
+ export async function handleBoards(client, _params, baseUrl) {
20
+ try {
21
+ const boards = await client.listBoards();
22
+ const formatted = boards.map(board => ({
23
+ id: board._id,
24
+ name: board.name,
25
+ description: board.description || '',
26
+ nodeCount: board.nodeCount ?? 0,
27
+ updatedAt: formatShortDate(board.updatedAt),
28
+ link: getBoardUrl(baseUrl, board._id),
29
+ }));
30
+ return {
31
+ content: [
32
+ {
33
+ type: 'text',
34
+ text: formatBoardsTsv(formatted),
35
+ },
36
+ ],
37
+ };
38
+ }
39
+ catch (error) {
40
+ return {
41
+ content: [
42
+ {
43
+ type: 'text',
44
+ text: `Failed to list boards: ${formatError(error)}`,
45
+ },
46
+ ],
47
+ isError: true,
48
+ };
49
+ }
50
+ }
@@ -0,0 +1,55 @@
1
+ import { z } from 'zod';
2
+ import { findCollidingNodes, snapToGrid } from '../utils/collision.js';
3
+ import { formatError } from '../utils/errors.js';
4
+ export const checkCollisionSchema = z.object({
5
+ boardId: z.string().describe('The board ID to check against'),
6
+ x: z.number().describe('Left position of the rectangle'),
7
+ y: z.number().describe('Top position of the rectangle'),
8
+ width: z.number().describe('Width of the rectangle'),
9
+ height: z.number().describe('Height of the rectangle'),
10
+ excludeNodeId: z
11
+ .string()
12
+ .optional()
13
+ .describe('Optional node ID to exclude from check (for move operations)'),
14
+ });
15
+ export async function handleCheckCollision(client, params) {
16
+ try {
17
+ // Fetch nodes from board
18
+ const nodes = await client.listNodes({
19
+ boardId: params.boardId,
20
+ limit: 500,
21
+ });
22
+ // Snap position to grid
23
+ const rect = {
24
+ x: snapToGrid(params.x),
25
+ y: snapToGrid(params.y),
26
+ width: params.width,
27
+ height: params.height,
28
+ };
29
+ // Check for collisions
30
+ const result = findCollidingNodes(rect, nodes, params.excludeNodeId);
31
+ return {
32
+ content: [
33
+ {
34
+ type: 'text',
35
+ text: JSON.stringify({
36
+ collides: result.collides,
37
+ rect,
38
+ collidingNodes: result.collidingNodes,
39
+ }, null, 2),
40
+ },
41
+ ],
42
+ };
43
+ }
44
+ catch (error) {
45
+ return {
46
+ content: [
47
+ {
48
+ type: 'text',
49
+ text: `Failed to check collision: ${formatError(error)}`,
50
+ },
51
+ ],
52
+ isError: true,
53
+ };
54
+ }
55
+ }
@@ -0,0 +1,35 @@
1
+ import { z } from 'zod';
2
+ export const colorsSchema = z.object({});
3
+ const NODE_COLORS = [
4
+ { name: 'amber', rgb: 'rgb(253, 230, 138)' },
5
+ { name: 'blue', rgb: 'rgb(0, 120, 255)' },
6
+ { name: 'cyan', rgb: 'rgb(0, 231, 255)' },
7
+ { name: 'emerald', rgb: 'rgb(0, 230, 118)' },
8
+ { name: 'fuchsia', rgb: 'rgb(233, 30, 99)' },
9
+ { name: 'green', rgb: 'rgb(52, 211, 153)' },
10
+ { name: 'indigo', rgb: 'rgb(72, 77, 201)' },
11
+ { name: 'lime', rgb: 'rgb(209, 250, 229)' },
12
+ { name: 'orange', rgb: 'rgb(252, 165, 0)' },
13
+ { name: 'pink', rgb: 'rgb(244, 143, 177)' },
14
+ { name: 'purple', rgb: 'rgb(156, 39, 176)' },
15
+ { name: 'red', rgb: 'rgb(239, 68, 68)' },
16
+ { name: 'rose', rgb: 'rgb(255, 204, 203)' },
17
+ { name: 'sky', rgb: 'rgb(0, 184, 230)' },
18
+ { name: 'slate', rgb: 'rgb(107, 114, 128)' },
19
+ { name: 'teal', rgb: 'rgb(0, 210, 183)' },
20
+ { name: 'violet', rgb: 'rgb(123, 31, 162)' },
21
+ { name: 'yellow', rgb: 'rgb(255, 251, 235)' },
22
+ ];
23
+ export async function handleColors() {
24
+ return {
25
+ content: [
26
+ {
27
+ type: 'text',
28
+ text: JSON.stringify({
29
+ colors: NODE_COLORS,
30
+ usage: 'Use the color name (e.g., "blue", "emerald") when creating or updating nodes.',
31
+ }, null, 2),
32
+ },
33
+ ],
34
+ };
35
+ }
@@ -0,0 +1,50 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { z } from 'zod';
3
+ import { formatError } from '../utils/errors.js';
4
+ import { getBoardUrl } from '../utils/urls.js';
5
+ export const createBoardSchema = z.object({
6
+ name: z.string().min(1).describe('The name for the new board'),
7
+ });
8
+ function generateObjectId() {
9
+ const timestamp = Math.floor(Date.now() / 1000)
10
+ .toString(16)
11
+ .padStart(8, '0');
12
+ const random = randomUUID().replace(/-/g, '').slice(0, 16);
13
+ return timestamp + random;
14
+ }
15
+ export async function handleCreateBoard(client, params, baseUrl) {
16
+ try {
17
+ const boardId = generateObjectId();
18
+ const board = await client.createBoard({
19
+ _id: boardId,
20
+ name: params.name,
21
+ });
22
+ const result = {
23
+ success: true,
24
+ board: {
25
+ id: board._id,
26
+ name: board.name,
27
+ link: getBoardUrl(baseUrl, board._id),
28
+ },
29
+ };
30
+ return {
31
+ content: [
32
+ {
33
+ type: 'text',
34
+ text: JSON.stringify(result, null, 2),
35
+ },
36
+ ],
37
+ };
38
+ }
39
+ catch (error) {
40
+ return {
41
+ content: [
42
+ {
43
+ type: 'text',
44
+ text: `Failed to create board: ${formatError(error)}`,
45
+ },
46
+ ],
47
+ isError: true,
48
+ };
49
+ }
50
+ }