@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.
- package/README.md +100 -0
- package/dist/client/helene.js +178 -0
- package/dist/client/types.js +27 -0
- package/dist/index.js +26 -0
- package/dist/tools/batch-update.js +118 -0
- package/dist/tools/boards.js +50 -0
- package/dist/tools/check-collision.js +55 -0
- package/dist/tools/colors.js +35 -0
- package/dist/tools/create-board.js +50 -0
- package/dist/tools/create-edge.js +61 -0
- package/dist/tools/create-graph.js +174 -0
- package/dist/tools/create.js +152 -0
- package/dist/tools/delete-edge.js +36 -0
- package/dist/tools/delete.js +36 -0
- package/dist/tools/extract-board-content.js +207 -0
- package/dist/tools/find-position.js +117 -0
- package/dist/tools/grid-calc.js +205 -0
- package/dist/tools/index.js +940 -0
- package/dist/tools/link.js +22 -0
- package/dist/tools/list-edges.js +58 -0
- package/dist/tools/list-nodes.js +96 -0
- package/dist/tools/list-positions.js +64 -0
- package/dist/tools/node-types.js +65 -0
- package/dist/tools/patch-content.js +143 -0
- package/dist/tools/read.js +41 -0
- package/dist/tools/search-board.js +99 -0
- package/dist/tools/search-boards.js +50 -0
- package/dist/tools/search.js +89 -0
- package/dist/tools/size-calc.js +108 -0
- package/dist/tools/update-board.js +64 -0
- package/dist/tools/update-edge.js +63 -0
- package/dist/tools/update.js +157 -0
- package/dist/utils/collision.js +177 -0
- package/dist/utils/config.js +16 -0
- package/dist/utils/ejson.js +30 -0
- package/dist/utils/errors.js +16 -0
- package/dist/utils/formatting.js +15 -0
- package/dist/utils/urls.js +18 -0
- 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
|
+
}
|