@kineviz/graphxr-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 (3) hide show
  1. package/README.md +168 -0
  2. package/dist/index.js +870 -0
  3. package/package.json +41 -0
package/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # @kineviz/graphxr-mcp
2
+
3
+ > **ALPHA SOFTWARE - EXPERIMENTAL**
4
+ >
5
+ > This MCP server is in early alpha stage. APIs may change without notice. Do not use in production environments. Use at your own risk.
6
+
7
+ MCP (Model Context Protocol) server for [GraphXR](https://graphxr.kineviz.com) - enables Claude and Cursor to create graph visualizations programmatically.
8
+
9
+ ## Features
10
+
11
+ - Create and manage GraphXR projects
12
+ - Execute JavaScript using the GraphXR API (`gxr`)
13
+ - Take screenshots of graph visualizations
14
+ - Export graph data
15
+ - Clear and manipulate graph canvas
16
+
17
+ ## Prerequisites
18
+
19
+ - Node.js 18 or later
20
+ - A GraphXR account with an API key
21
+ - Playwright Chromium browser (installed automatically or manually)
22
+
23
+ ## Quick Start
24
+
25
+ ### 1. Get Your API Key
26
+
27
+ 1. Log in to [GraphXR](https://graphxr.kineviz.com)
28
+ 2. Click your profile icon in the top right
29
+ 3. Select "API Key" from the dropdown
30
+ 4. Copy your API key
31
+
32
+ ### 2. Install Playwright Browser
33
+
34
+ The MCP server uses Playwright to interact with GraphXR. You need to install the Chromium browser:
35
+
36
+ ```bash
37
+ npx playwright install chromium
38
+ ```
39
+
40
+ ### 3. Configure Your MCP Client
41
+
42
+ #### Cursor IDE
43
+
44
+ Create or edit `.cursor/mcp.json` in your project or home directory:
45
+
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "graphxr": {
50
+ "command": "npx",
51
+ "args": ["-y", "@kineviz/graphxr-mcp"],
52
+ "env": {
53
+ "GRAPHXR_API_KEY": "your-api-key-here",
54
+ "GRAPHXR_URL": "https://graphxr.kineviz.com"
55
+ }
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ #### Claude Desktop
62
+
63
+ Add to your Claude Desktop configuration:
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "graphxr": {
69
+ "command": "npx",
70
+ "args": ["-y", "@kineviz/graphxr-mcp"],
71
+ "env": {
72
+ "GRAPHXR_API_KEY": "your-api-key-here",
73
+ "GRAPHXR_URL": "https://graphxr.kineviz.com"
74
+ }
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ ## Environment Variables
81
+
82
+ | Variable | Required | Default | Description |
83
+ |----------|----------|---------|-------------|
84
+ | `GRAPHXR_API_KEY` | Yes | - | Your GraphXR API key |
85
+ | `GRAPHXR_URL` | No | `https://graphxr.kineviz.com` | GraphXR server URL |
86
+ | `HEADLESS` | No | `true` | Run browser in headless mode |
87
+ | `DEBUG` | No | `false` | Enable debug logging |
88
+
89
+ ## Available Tools
90
+
91
+ Once configured, you can ask Claude/Cursor to:
92
+
93
+ - **list_projects** - List all your GraphXR projects
94
+ - **create_project** - Create a new project
95
+ - **open_project** - Open a project in the browser session
96
+ - **run_javascript** - Execute JavaScript code using the `gxr` API
97
+ - **screenshot** - Take a screenshot of the current graph
98
+ - **export_graph** - Export graph data as JSON
99
+ - **clear_graph** - Clear all nodes and edges from the graph
100
+ - **close_browser** - Close the browser session
101
+
102
+ ## Example Usage
103
+
104
+ Ask Claude or Cursor:
105
+
106
+ > "Create a new GraphXR project called 'My Network' and add some sample nodes"
107
+
108
+ > "Take a screenshot of the current graph"
109
+
110
+ > "Run this JavaScript to create 5 connected nodes"
111
+
112
+ ## Troubleshooting
113
+
114
+ ### "Playwright browser not found"
115
+
116
+ Run `npx playwright install chromium` to install the required browser.
117
+
118
+ ### "API key invalid"
119
+
120
+ 1. Verify your API key is correct
121
+ 2. Check that your GraphXR account is active
122
+ 3. Ensure the `GRAPHXR_URL` matches your GraphXR server
123
+
124
+ ### "Connection refused"
125
+
126
+ 1. Check your internet connection
127
+ 2. Verify the `GRAPHXR_URL` is accessible
128
+ 3. Check if GraphXR is experiencing downtime
129
+
130
+ ### Debug Mode
131
+
132
+ Enable debug logging to see detailed output:
133
+
134
+ ```json
135
+ {
136
+ "env": {
137
+ "DEBUG": "true"
138
+ }
139
+ }
140
+ ```
141
+
142
+ ## GraphXR API Reference
143
+
144
+ The `run_javascript` tool executes code with access to the `gxr` global object. Common operations:
145
+
146
+ ```javascript
147
+ // Add a node
148
+ gxr.add({ nodes: [{ category: 'Person', properties: { name: 'Alice' } }] });
149
+
150
+ // Get all nodes
151
+ const nodes = gxr.getNodes();
152
+
153
+ // Run a layout
154
+ gxr.layout.spring();
155
+
156
+ // Take a screenshot (returns base64)
157
+ const screenshot = await gxr.screenshot();
158
+ ```
159
+
160
+ For full API documentation, see the [GraphXR API Reference](https://graphxr.kineviz.com/docs).
161
+
162
+ ## License
163
+
164
+ MIT
165
+
166
+ ## Support
167
+
168
+ For issues and feature requests, contact [Kineviz](https://kineviz.com).
package/dist/index.js ADDED
@@ -0,0 +1,870 @@
1
+ #!/usr/bin/env node
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ GraphXRMCPServer: () => GraphXRMCPServer
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+ var import_server = require("@modelcontextprotocol/sdk/server/index.js");
37
+ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
38
+ var import_types = require("@modelcontextprotocol/sdk/types.js");
39
+
40
+ // graphxr-client.ts
41
+ var import_node_fetch = __toESM(require("node-fetch"));
42
+ var DEBUG = process.env.DEBUG === "true" || process.env.GRAPHXR_DEBUG === "true";
43
+ function debug(...args) {
44
+ if (DEBUG) {
45
+ console.error("[GraphXR Client]", (/* @__PURE__ */ new Date()).toISOString(), ...args);
46
+ }
47
+ }
48
+ var GraphXRClient = class {
49
+ constructor(config) {
50
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
51
+ this.apiKey = config.apiKey;
52
+ }
53
+ /**
54
+ * Make an authenticated request to the GraphXR API
55
+ */
56
+ async request(method, endpoint, body) {
57
+ const url = `${this.baseUrl}${endpoint}`;
58
+ debug("request:", method, url);
59
+ debug("request: body =", body ? JSON.stringify(body) : "(none)");
60
+ const headers = {
61
+ "Content-Type": "application/json",
62
+ "x-api-key": this.apiKey
63
+ };
64
+ const options = {
65
+ method,
66
+ headers
67
+ };
68
+ if (body) {
69
+ options.body = JSON.stringify(body);
70
+ }
71
+ debug("request: sending...");
72
+ const response = await (0, import_node_fetch.default)(url, options);
73
+ debug("request: response status =", response.status, response.statusText);
74
+ if (!response.ok) {
75
+ const errorText = await response.text();
76
+ debug("request: error response body =", errorText);
77
+ throw new Error(`API request failed: ${response.status} ${response.statusText}`);
78
+ }
79
+ const data = await response.json();
80
+ debug("request: response data.status =", data.status, "message =", data.message);
81
+ if (data.status !== 0) {
82
+ throw new Error(`API error: ${data.message}`);
83
+ }
84
+ return data;
85
+ }
86
+ /**
87
+ * Verify the API key is valid by making a test request
88
+ */
89
+ async verifyApiKey() {
90
+ try {
91
+ await this.listProjects();
92
+ return true;
93
+ } catch (error) {
94
+ return false;
95
+ }
96
+ }
97
+ /**
98
+ * List all projects accessible to the authenticated user
99
+ */
100
+ async listProjects() {
101
+ debug("listProjects: calling API");
102
+ const response = await this.request(
103
+ "POST",
104
+ "/api/graph/neo4j/project/list",
105
+ {}
106
+ );
107
+ debug("listProjects: got", response.content?.length ?? 0, "projects");
108
+ return response.content || [];
109
+ }
110
+ /**
111
+ * Get detailed information about a specific project
112
+ */
113
+ async getProject(projectId) {
114
+ try {
115
+ const response = await this.request(
116
+ "GET",
117
+ `/api/graph/neo4j/project/getInfo?id=${encodeURIComponent(projectId)}`
118
+ );
119
+ return response.content || null;
120
+ } catch (error) {
121
+ return null;
122
+ }
123
+ }
124
+ /**
125
+ * Create a new project
126
+ */
127
+ async createProject(options) {
128
+ const dbinfo = {
129
+ projectName: options.projectName,
130
+ databaseType: options.databaseType || "kuzu",
131
+ isShare: options.isShare ?? true,
132
+ hostname: "",
133
+ isLocal: false
134
+ };
135
+ const response = await this.request(
136
+ "POST",
137
+ "/api/graph/neo4j/project/add",
138
+ { dbinfo }
139
+ );
140
+ if (!response.content) {
141
+ throw new Error("Failed to create project: no content returned");
142
+ }
143
+ return response.content;
144
+ }
145
+ /**
146
+ * Delete a project
147
+ */
148
+ async deleteProject(projectId) {
149
+ await this.request(
150
+ "POST",
151
+ "/api/graph/neo4j/project/delete",
152
+ { id: projectId }
153
+ );
154
+ }
155
+ /**
156
+ * Get the URL to access a project in the browser
157
+ */
158
+ getProjectUrl(project) {
159
+ const encodedName = encodeURIComponent(project.projectName);
160
+ return `${this.baseUrl}/p/${project._id}/${encodedName}`;
161
+ }
162
+ /**
163
+ * Get the URL to access a project with API key authentication
164
+ */
165
+ getProjectUrlWithAuth(project) {
166
+ const baseProjectUrl = this.getProjectUrl(project);
167
+ return `${baseProjectUrl}?apiKey=${encodeURIComponent(this.apiKey)}`;
168
+ }
169
+ /**
170
+ * Get the base URL of the GraphXR instance
171
+ */
172
+ getBaseUrl() {
173
+ return this.baseUrl;
174
+ }
175
+ /**
176
+ * Get the API key (for passing to browser sessions)
177
+ */
178
+ getApiKey() {
179
+ return this.apiKey;
180
+ }
181
+ };
182
+
183
+ // browser-session.ts
184
+ var import_playwright = require("playwright");
185
+ var DEBUG2 = process.env.DEBUG === "true" || process.env.GRAPHXR_DEBUG === "true";
186
+ function debug2(...args) {
187
+ if (DEBUG2) {
188
+ console.error("[Browser Session]", (/* @__PURE__ */ new Date()).toISOString(), ...args);
189
+ }
190
+ }
191
+ var BrowserSession = class {
192
+ constructor(client, config = {}) {
193
+ this.browser = null;
194
+ this.context = null;
195
+ this.page = null;
196
+ this.currentProject = null;
197
+ this.client = client;
198
+ this.config = {
199
+ headless: config.headless ?? true,
200
+ timeout: config.timeout ?? 6e4
201
+ };
202
+ }
203
+ /**
204
+ * Check if the browser session is active
205
+ */
206
+ isActive() {
207
+ return this.browser !== null && this.page !== null;
208
+ }
209
+ /**
210
+ * Get the currently open project
211
+ */
212
+ getCurrentProject() {
213
+ return this.currentProject;
214
+ }
215
+ /**
216
+ * Launch the browser and open a project
217
+ */
218
+ async openProject(project) {
219
+ if (this.isActive()) {
220
+ await this.close();
221
+ }
222
+ debug2("Launching browser, headless:", this.config.headless);
223
+ this.browser = await import_playwright.chromium.launch({
224
+ headless: this.config.headless
225
+ });
226
+ this.context = await this.browser.newContext({
227
+ viewport: { width: 1920, height: 1080 },
228
+ ignoreHTTPSErrors: true
229
+ });
230
+ this.page = await this.context.newPage();
231
+ await this.setupApiKeyInjection();
232
+ const projectUrl = this.client.getProjectUrl(project);
233
+ debug2("Navigating to project:", projectUrl);
234
+ await this.page.goto(projectUrl, {
235
+ waitUntil: "networkidle",
236
+ timeout: this.config.timeout
237
+ });
238
+ await this.waitForGraphXRReady();
239
+ this.currentProject = project;
240
+ debug2("Project opened successfully:", project.projectName);
241
+ }
242
+ /**
243
+ * Set up route interception to automatically inject API key for same-origin requests.
244
+ * This ensures all requests to the GraphXR server are authenticated without
245
+ * leaking the API key to external services.
246
+ */
247
+ async setupApiKeyInjection() {
248
+ if (!this.page) {
249
+ throw new Error("No active page");
250
+ }
251
+ const graphxrOrigin = new URL(this.client.getBaseUrl()).origin;
252
+ const apiKey = this.client.getApiKey();
253
+ debug2("Setting up API key injection for origin:", graphxrOrigin);
254
+ await this.page.route("**/*", async (route) => {
255
+ const request = route.request();
256
+ const requestUrl = request.url();
257
+ let requestOrigin;
258
+ try {
259
+ requestOrigin = new URL(requestUrl).origin;
260
+ } catch {
261
+ debug2("Could not parse URL, passing through:", requestUrl);
262
+ await route.continue();
263
+ return;
264
+ }
265
+ if (requestOrigin === graphxrOrigin) {
266
+ const existingHeaders = request.headers();
267
+ if (!existingHeaders["x-api-key"]) {
268
+ debug2("Injecting API key for:", requestUrl);
269
+ await route.continue({
270
+ headers: {
271
+ ...existingHeaders,
272
+ "x-api-key": apiKey
273
+ }
274
+ });
275
+ } else {
276
+ debug2("API key already present, passing through:", requestUrl);
277
+ await route.continue();
278
+ }
279
+ } else {
280
+ debug2("External request, passing through:", requestUrl);
281
+ await route.continue();
282
+ }
283
+ });
284
+ }
285
+ /**
286
+ * Wait for GraphXR to be fully loaded and ready
287
+ */
288
+ async waitForGraphXRReady() {
289
+ if (!this.page) {
290
+ throw new Error("No active page");
291
+ }
292
+ await this.page.waitForFunction(
293
+ () => {
294
+ const win = window;
295
+ return win.gxr && typeof win.gxr === "function";
296
+ },
297
+ { timeout: this.config.timeout }
298
+ );
299
+ await this.page.waitForFunction(
300
+ () => {
301
+ const win = window;
302
+ return win.gxr && win.gxr.isCanvasLoaded && win.gxr.isCanvasLoaded();
303
+ },
304
+ { timeout: this.config.timeout }
305
+ );
306
+ await this.page.waitForTimeout(1e3);
307
+ }
308
+ /**
309
+ * Execute JavaScript code in the GraphXR context
310
+ */
311
+ async executeJavaScript(code) {
312
+ if (!this.page) {
313
+ return {
314
+ success: false,
315
+ error: "No active browser session. Call openProject first."
316
+ };
317
+ }
318
+ try {
319
+ const result = await this.page.evaluate(async (jsCode) => {
320
+ const win = window;
321
+ const gxr = win.gxr;
322
+ if (!gxr) {
323
+ throw new Error("GraphXR API (gxr) not available");
324
+ }
325
+ const AsyncFunction = Object.getPrototypeOf(async function() {
326
+ }).constructor;
327
+ const fn = new AsyncFunction("gxr", jsCode);
328
+ return await fn(gxr);
329
+ }, code);
330
+ return {
331
+ success: true,
332
+ result
333
+ };
334
+ } catch (error) {
335
+ return {
336
+ success: false,
337
+ error: error instanceof Error ? error.message : String(error)
338
+ };
339
+ }
340
+ }
341
+ /**
342
+ * Take a screenshot of the GraphXR canvas
343
+ */
344
+ async screenshot(options = {}) {
345
+ if (!this.page) {
346
+ throw new Error("No active browser session. Call openProject first.");
347
+ }
348
+ const {
349
+ frameNodes = true,
350
+ hidePanels = true,
351
+ includeLegends = false,
352
+ format = "png",
353
+ quality = 1
354
+ } = options;
355
+ const base64Data = await this.page.evaluate(
356
+ async (opts) => {
357
+ const win = window;
358
+ const gxr = win.gxr;
359
+ if (!gxr || !gxr.screenshot) {
360
+ throw new Error("GraphXR screenshot API not available");
361
+ }
362
+ const blob = await gxr.screenshot({
363
+ frameNodes: opts.frameNodes,
364
+ hidePanels: opts.hidePanels,
365
+ includeLegends: opts.includeLegends,
366
+ format: opts.format,
367
+ quality: opts.quality
368
+ });
369
+ return new Promise((resolve, reject) => {
370
+ const reader = new FileReader();
371
+ reader.onloadend = () => {
372
+ const result = reader.result;
373
+ const base64 = result.split(",")[1];
374
+ resolve(base64);
375
+ };
376
+ reader.onerror = reject;
377
+ reader.readAsDataURL(blob);
378
+ });
379
+ },
380
+ { frameNodes, hidePanels, includeLegends, format, quality }
381
+ );
382
+ return Buffer.from(base64Data, "base64");
383
+ }
384
+ /**
385
+ * Export the graph data in the specified format
386
+ */
387
+ async exportGraph(options) {
388
+ if (!this.page) {
389
+ throw new Error("No active browser session. Call openProject first.");
390
+ }
391
+ const { format, filename } = options;
392
+ if (format === "json") {
393
+ const result = await this.executeJavaScript(`
394
+ const snapshot = gxr.snapshot();
395
+ return JSON.stringify(snapshot, null, 2);
396
+ `);
397
+ if (!result.success) {
398
+ throw new Error(`Export failed: ${result.error}`);
399
+ }
400
+ return {
401
+ data: result.result,
402
+ filename: filename || `graph-export-${Date.now()}.json`
403
+ };
404
+ }
405
+ const exportResult = await this.page.evaluate(
406
+ async (exportFormat) => {
407
+ const win = window;
408
+ const controller = win._app?.controller;
409
+ if (!controller || !controller.exportGraphXR) {
410
+ throw new Error("GraphXR export API not available");
411
+ }
412
+ const gxr = win.gxr;
413
+ const snapshot = gxr.snapshot();
414
+ return JSON.stringify(snapshot, null, 2);
415
+ },
416
+ format
417
+ );
418
+ return {
419
+ data: exportResult,
420
+ filename: filename || `graph-export-${Date.now()}.${format === "graphxr" ? "gxrf" : format}`
421
+ };
422
+ }
423
+ /**
424
+ * Clear the graph
425
+ */
426
+ async clearGraph() {
427
+ const result = await this.executeJavaScript("gxr.clear(); return true;");
428
+ if (!result.success) {
429
+ throw new Error(`Failed to clear graph: ${result.error}`);
430
+ }
431
+ }
432
+ /**
433
+ * Close the browser session
434
+ */
435
+ async close() {
436
+ if (this.page) {
437
+ await this.page.close();
438
+ this.page = null;
439
+ }
440
+ if (this.context) {
441
+ await this.context.close();
442
+ this.context = null;
443
+ }
444
+ if (this.browser) {
445
+ await this.browser.close();
446
+ this.browser = null;
447
+ }
448
+ this.currentProject = null;
449
+ }
450
+ };
451
+
452
+ // index.ts
453
+ var GRAPHXR_API_KEY = process.env.GRAPHXR_API_KEY || "";
454
+ var GRAPHXR_URL = process.env.GRAPHXR_URL || "http://localhost:9000";
455
+ var HEADLESS = process.env.GRAPHXR_HEADLESS !== "false";
456
+ var DEBUG3 = process.env.DEBUG === "true" || process.env.GRAPHXR_DEBUG === "true";
457
+ function debug3(...args) {
458
+ if (DEBUG3) {
459
+ console.error("[MCP DEBUG]", (/* @__PURE__ */ new Date()).toISOString(), ...args);
460
+ }
461
+ }
462
+ var tools = [
463
+ {
464
+ name: "list_projects",
465
+ description: "List all GraphXR projects accessible to the authenticated user",
466
+ inputSchema: {
467
+ type: "object",
468
+ properties: {},
469
+ required: []
470
+ }
471
+ },
472
+ {
473
+ name: "create_project",
474
+ description: "Create a new GraphXR project",
475
+ inputSchema: {
476
+ type: "object",
477
+ properties: {
478
+ projectName: {
479
+ type: "string",
480
+ description: "Name for the new project"
481
+ },
482
+ databaseType: {
483
+ type: "string",
484
+ description: "Database type (default: kuzu)",
485
+ enum: ["kuzu", "neo4j"]
486
+ }
487
+ },
488
+ required: ["projectName"]
489
+ }
490
+ },
491
+ {
492
+ name: "open_project",
493
+ description: "Open a GraphXR project in a headless browser session for interaction",
494
+ inputSchema: {
495
+ type: "object",
496
+ properties: {
497
+ projectId: {
498
+ type: "string",
499
+ description: "The project ID to open"
500
+ }
501
+ },
502
+ required: ["projectId"]
503
+ }
504
+ },
505
+ {
506
+ name: "run_javascript",
507
+ description: `Execute JavaScript code in the GraphXR canvas context using the gxr API.
508
+ The code has access to the 'gxr' object which provides methods for:
509
+ - Graph manipulation: gxr.addNode(), gxr.addEdge(), gxr.nodes(), gxr.edges(), gxr.clear()
510
+ - Layout: gxr.forceLayout(), gxr.circle(), gxr.grid(), gxr.ego(), gxr.parametric()
511
+ - Styling: gxr.colorNodesByProperty(), gxr.sizeNodesByProperty(), gxr.setCategoryColor()
512
+ - Camera: gxr.flyToCenter(), gxr.zoomIn(), gxr.zoomOut()
513
+ - Transform: gxr.extract(), gxr.link(), gxr.merge(), gxr.aggregate()
514
+ - Query: gxr.query() for Cypher queries
515
+ The code should return a value that can be serialized to JSON.`,
516
+ inputSchema: {
517
+ type: "object",
518
+ properties: {
519
+ code: {
520
+ type: "string",
521
+ description: "JavaScript code to execute. Has access to gxr API. Must return a value."
522
+ }
523
+ },
524
+ required: ["code"]
525
+ }
526
+ },
527
+ {
528
+ name: "screenshot",
529
+ description: "Take a screenshot of the GraphXR canvas",
530
+ inputSchema: {
531
+ type: "object",
532
+ properties: {
533
+ frameNodes: {
534
+ type: "boolean",
535
+ description: "Whether to frame all nodes in view before capturing (default: true)"
536
+ },
537
+ hidePanels: {
538
+ type: "boolean",
539
+ description: "Whether to hide UI panels (default: true)"
540
+ },
541
+ includeLegends: {
542
+ type: "boolean",
543
+ description: "Whether to include legend overlays (default: false)"
544
+ }
545
+ },
546
+ required: []
547
+ }
548
+ },
549
+ {
550
+ name: "export_graph",
551
+ description: "Export the current graph data",
552
+ inputSchema: {
553
+ type: "object",
554
+ properties: {
555
+ format: {
556
+ type: "string",
557
+ description: "Export format",
558
+ enum: ["json", "graphxr", "csv"]
559
+ }
560
+ },
561
+ required: ["format"]
562
+ }
563
+ },
564
+ {
565
+ name: "close_project",
566
+ description: "Close the current browser session",
567
+ inputSchema: {
568
+ type: "object",
569
+ properties: {},
570
+ required: []
571
+ }
572
+ }
573
+ ];
574
+ var GraphXRMCPServer = class {
575
+ constructor() {
576
+ this.client = null;
577
+ this.browserSession = null;
578
+ this.server = new import_server.Server(
579
+ {
580
+ name: "graphxr-mcp-server",
581
+ version: "1.0.0"
582
+ },
583
+ {
584
+ capabilities: {
585
+ tools: {}
586
+ }
587
+ }
588
+ );
589
+ this.setupHandlers();
590
+ }
591
+ /**
592
+ * Initialize the GraphXR client with API key
593
+ */
594
+ initializeClient() {
595
+ if (!GRAPHXR_API_KEY) {
596
+ throw new Error(
597
+ "GRAPHXR_API_KEY environment variable is required. Generate an API key in GraphXR Settings > API Access."
598
+ );
599
+ }
600
+ if (!this.client) {
601
+ this.client = new GraphXRClient({
602
+ baseUrl: GRAPHXR_URL,
603
+ apiKey: GRAPHXR_API_KEY
604
+ });
605
+ }
606
+ return this.client;
607
+ }
608
+ /**
609
+ * Get or create the browser session
610
+ */
611
+ getBrowserSession() {
612
+ const client = this.initializeClient();
613
+ if (!this.browserSession) {
614
+ this.browserSession = new BrowserSession(client, {
615
+ headless: HEADLESS
616
+ });
617
+ }
618
+ return this.browserSession;
619
+ }
620
+ /**
621
+ * Setup MCP request handlers
622
+ */
623
+ setupHandlers() {
624
+ this.server.setRequestHandler(import_types.ListToolsRequestSchema, async () => {
625
+ return { tools };
626
+ });
627
+ this.server.setRequestHandler(import_types.CallToolRequestSchema, async (request) => {
628
+ const { name, arguments: args } = request.params;
629
+ debug3("Tool call received:", name);
630
+ debug3("Tool arguments:", JSON.stringify(args));
631
+ try {
632
+ switch (name) {
633
+ case "list_projects":
634
+ return await this.handleListProjects();
635
+ case "create_project":
636
+ return await this.handleCreateProject(args);
637
+ case "open_project":
638
+ return await this.handleOpenProject(args);
639
+ case "run_javascript":
640
+ return await this.handleRunJavaScript(args);
641
+ case "screenshot":
642
+ return await this.handleScreenshot(args);
643
+ case "export_graph":
644
+ return await this.handleExportGraph(args);
645
+ case "close_project":
646
+ return await this.handleCloseProject();
647
+ default:
648
+ throw new Error(`Unknown tool: ${name}`);
649
+ }
650
+ } catch (error) {
651
+ const errorMessage = error instanceof Error ? error.message : String(error);
652
+ const errorStack = error instanceof Error ? error.stack : void 0;
653
+ debug3("Tool error:", errorMessage);
654
+ if (errorStack) {
655
+ debug3("Stack trace:", errorStack);
656
+ }
657
+ return {
658
+ content: [
659
+ {
660
+ type: "text",
661
+ text: `Error: ${errorMessage}`
662
+ }
663
+ ],
664
+ isError: true
665
+ };
666
+ }
667
+ });
668
+ }
669
+ /**
670
+ * Handle list_projects tool
671
+ */
672
+ async handleListProjects() {
673
+ debug3("handleListProjects: starting");
674
+ debug3("handleListProjects: GRAPHXR_URL =", GRAPHXR_URL);
675
+ debug3("handleListProjects: API key length =", GRAPHXR_API_KEY.length);
676
+ const client = this.initializeClient();
677
+ debug3("handleListProjects: client initialized");
678
+ debug3("handleListProjects: calling listProjects API...");
679
+ const projects = await client.listProjects();
680
+ debug3("handleListProjects: received", projects.length, "projects");
681
+ const projectList = projects.map((p) => ({
682
+ id: p._id,
683
+ name: p.projectName,
684
+ databaseType: p.databaseType,
685
+ isShare: p.isShare,
686
+ description: p.projectSettings?.description || ""
687
+ }));
688
+ debug3("handleListProjects: returning project list");
689
+ return {
690
+ content: [
691
+ {
692
+ type: "text",
693
+ text: JSON.stringify(projectList, null, 2)
694
+ }
695
+ ]
696
+ };
697
+ }
698
+ /**
699
+ * Handle create_project tool
700
+ */
701
+ async handleCreateProject(args) {
702
+ const client = this.initializeClient();
703
+ const project = await client.createProject({
704
+ projectName: args.projectName,
705
+ databaseType: args.databaseType || "kuzu"
706
+ });
707
+ return {
708
+ content: [
709
+ {
710
+ type: "text",
711
+ text: JSON.stringify(
712
+ {
713
+ id: project._id,
714
+ name: project.projectName,
715
+ databaseType: project.databaseType,
716
+ url: client.getProjectUrl(project)
717
+ },
718
+ null,
719
+ 2
720
+ )
721
+ }
722
+ ]
723
+ };
724
+ }
725
+ /**
726
+ * Handle open_project tool
727
+ */
728
+ async handleOpenProject(args) {
729
+ const client = this.initializeClient();
730
+ const project = await client.getProject(args.projectId);
731
+ debug3("handleOpenProject: got project", project?._id, project?.projectName);
732
+ if (!project) {
733
+ throw new Error(`Project not found: ${args.projectId}`);
734
+ }
735
+ const session = this.getBrowserSession();
736
+ debug3("handleOpenProject: opening project in browser");
737
+ await session.openProject(project);
738
+ return {
739
+ content: [
740
+ {
741
+ type: "text",
742
+ text: `Opened project "${project.projectName}" (${project._id}). You can now run JavaScript code using the run_javascript tool.`
743
+ }
744
+ ]
745
+ };
746
+ }
747
+ /**
748
+ * Handle run_javascript tool
749
+ */
750
+ async handleRunJavaScript(args) {
751
+ const session = this.getBrowserSession();
752
+ if (!session.isActive()) {
753
+ throw new Error("No project is currently open. Use open_project first.");
754
+ }
755
+ const result = await session.executeJavaScript(args.code);
756
+ if (!result.success) {
757
+ throw new Error(result.error || "JavaScript execution failed");
758
+ }
759
+ return {
760
+ content: [
761
+ {
762
+ type: "text",
763
+ text: result.result !== void 0 ? JSON.stringify(result.result, null, 2) : "Code executed successfully (no return value)"
764
+ }
765
+ ]
766
+ };
767
+ }
768
+ /**
769
+ * Handle screenshot tool
770
+ */
771
+ async handleScreenshot(args) {
772
+ const session = this.getBrowserSession();
773
+ if (!session.isActive()) {
774
+ throw new Error("No project is currently open. Use open_project first.");
775
+ }
776
+ const imageBuffer = await session.screenshot(args);
777
+ const base64Image = imageBuffer.toString("base64");
778
+ return {
779
+ content: [
780
+ {
781
+ type: "image",
782
+ data: base64Image,
783
+ mimeType: args.format === "jpeg" ? "image/jpeg" : "image/png"
784
+ }
785
+ ]
786
+ };
787
+ }
788
+ /**
789
+ * Handle export_graph tool
790
+ */
791
+ async handleExportGraph(args) {
792
+ const session = this.getBrowserSession();
793
+ if (!session.isActive()) {
794
+ throw new Error("No project is currently open. Use open_project first.");
795
+ }
796
+ const exportResult = await session.exportGraph({
797
+ format: args.format
798
+ });
799
+ return {
800
+ content: [
801
+ {
802
+ type: "text",
803
+ text: `Export complete. Filename: ${exportResult.filename}
804
+
805
+ Data:
806
+ ${exportResult.data}`
807
+ }
808
+ ]
809
+ };
810
+ }
811
+ /**
812
+ * Handle close_project tool
813
+ */
814
+ async handleCloseProject() {
815
+ if (this.browserSession) {
816
+ await this.browserSession.close();
817
+ this.browserSession = null;
818
+ }
819
+ return {
820
+ content: [
821
+ {
822
+ type: "text",
823
+ text: "Browser session closed."
824
+ }
825
+ ]
826
+ };
827
+ }
828
+ /**
829
+ * Start the MCP server
830
+ */
831
+ async start() {
832
+ const transport = new import_stdio.StdioServerTransport();
833
+ await this.server.connect(transport);
834
+ process.on("SIGINT", async () => {
835
+ await this.cleanup();
836
+ process.exit(0);
837
+ });
838
+ process.on("SIGTERM", async () => {
839
+ await this.cleanup();
840
+ process.exit(0);
841
+ });
842
+ }
843
+ /**
844
+ * Cleanup resources
845
+ */
846
+ async cleanup() {
847
+ if (this.browserSession) {
848
+ await this.browserSession.close();
849
+ }
850
+ }
851
+ };
852
+ async function main() {
853
+ debug3("Starting GraphXR MCP Server");
854
+ debug3("Configuration:");
855
+ debug3(" GRAPHXR_URL:", GRAPHXR_URL);
856
+ debug3(" GRAPHXR_API_KEY:", GRAPHXR_API_KEY ? `(${GRAPHXR_API_KEY.length} chars)` : "(not set)");
857
+ debug3(" HEADLESS:", HEADLESS);
858
+ debug3(" DEBUG:", DEBUG3);
859
+ const server = new GraphXRMCPServer();
860
+ await server.start();
861
+ debug3("MCP Server started and listening on stdio");
862
+ }
863
+ main().catch((error) => {
864
+ console.error("Failed to start GraphXR MCP Server:", error);
865
+ process.exit(1);
866
+ });
867
+ // Annotate the CommonJS export names for ESM import in node:
868
+ 0 && (module.exports = {
869
+ GraphXRMCPServer
870
+ });
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@kineviz/graphxr-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for GraphXR - enables Claude/Cursor to create graph visualizations",
5
+ "bin": {
6
+ "graphxr-mcp": "./dist/index.js"
7
+ },
8
+ "files": [
9
+ "dist",
10
+ "README.md"
11
+ ],
12
+ "scripts": {
13
+ "build": "node build-package.js",
14
+ "prepublishOnly": "yarn build"
15
+ },
16
+ "dependencies": {
17
+ "@modelcontextprotocol/sdk": "^1.0.0",
18
+ "playwright": "^1.53.1",
19
+ "node-fetch": "^2.7.0"
20
+ },
21
+ "engines": {
22
+ "node": ">=18.0.0"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/kineviz/graphxr.git"
30
+ },
31
+ "keywords": [
32
+ "mcp",
33
+ "graphxr",
34
+ "model-context-protocol",
35
+ "claude",
36
+ "cursor",
37
+ "graph-visualization"
38
+ ],
39
+ "author": "Kineviz",
40
+ "license": "MIT"
41
+ }