@lightdash-tools/mcp 0.1.0 → 0.1.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.
package/README.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  MCP server for Lightdash: exposes projects, charts, dashboards, spaces, users, and groups as tools. Uses `@lightdash-tools/client` for all API access.
4
4
 
5
+ ## Installation
6
+
7
+ You can run the MCP server using `npx`:
8
+
9
+ ```bash
10
+ npx @lightdash-tools/mcp
11
+ ```
12
+
13
+ Or install it globally:
14
+
15
+ ```bash
16
+ npm install -g @lightdash-tools/mcp
17
+ ```
18
+
5
19
  ## Transports
6
20
 
7
21
  - **Stdio** — for local use (e.g. Claude Desktop, IDE). One process per client.
@@ -24,20 +38,24 @@ MCP server for Lightdash: exposes projects, charts, dashboards, spaces, users, a
24
38
 
25
39
  ### Stdio (local)
26
40
 
41
+ For use with Claude Desktop or IDEs, use `npx`:
42
+
43
+ ```bash
44
+ npx @lightdash-tools/mcp
45
+ ```
46
+
47
+ Or if installed globally:
48
+
27
49
  ```bash
28
- pnpm build
29
- pnpm start
30
- # or: node dist/index.js
50
+ lightdash-mcp
31
51
  ```
32
52
 
33
- Use with Claude Desktop or an IDE by configuring the MCP server command (e.g. `node` with path to `dist/index.js`). Logging goes to stderr only; stdout is JSON-RPC.
53
+ Logging goes to stderr only; stdout is JSON-RPC.
34
54
 
35
55
  ### Streamable HTTP (remote)
36
56
 
37
57
  ```bash
38
- pnpm build
39
- pnpm start:http
40
- # or: node dist/http.js
58
+ npx @lightdash-tools/mcp --http
41
59
  ```
42
60
 
43
61
  The server listens on `http://localhost:3100` (or `MCP_HTTP_PORT`). MCP endpoint: `POST/GET/DELETE /mcp`. Sessions are created on first `initialize`; subsequent requests must include the `Mcp-Session-Id` header returned by the server.
@@ -46,8 +64,32 @@ With auth disabled (default), any client can call the endpoint. With `MCP_AUTH_E
46
64
 
47
65
  ## Tools
48
66
 
49
- Same set in both modes: `list_projects`, `get_project`, `list_charts`, `list_dashboards`, `list_spaces`, `get_space`, `list_organization_members`, `get_member`, `delete_member`, `list_groups`, `get_group`.
67
+ Same set in both modes: `list_projects`, `get_project`, `list_explores`, `get_explore`, `list_charts`, `list_charts_as_code`, `upsert_chart_as_code`, `list_dashboards`, `list_spaces`, `get_space`, `list_organization_members`, `get_member`, `delete_member`, `list_groups`, `get_group`, `compile_query`.
50
68
 
51
69
  ### Destructive tools
52
70
 
53
71
  Tools with `destructiveHint: true` (e.g. `delete_member`) perform irreversible or high-impact actions. MCP clients should show a warning and/or require user confirmation before executing them. AI agents should ask the user for explicit confirmation before calling such tools.
72
+
73
+ ## Testing
74
+
75
+ This package includes unit tests and integration tests. Integration tests run against a real Lightdash API and are only executed if the required environment variables are set.
76
+
77
+ ### Running unit tests
78
+
79
+ ```bash
80
+ pnpm test
81
+ ```
82
+
83
+ ### Running integration tests
84
+
85
+ To run tests against a real Lightdash instance, provide your credentials:
86
+
87
+ ```bash
88
+ LIGHTDASH_URL=https://app.lightdash.cloud LIGHTDASH_API_KEY=your_api_key pnpm test
89
+ ```
90
+
91
+ The integration tests will automatically detect these environment variables and run additional scenarios, such as verifying authentication and tool execution against the live API.
92
+
93
+ ## License
94
+
95
+ Apache-2.0
package/dist/bin.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP server CLI entrypoint.
4
+ */
5
+ declare const args: string[];
package/dist/bin.js ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * MCP server CLI entrypoint.
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ const args = process.argv.slice(2);
40
+ if (args.includes('--http')) {
41
+ void Promise.resolve().then(() => __importStar(require('./http.js')));
42
+ }
43
+ else {
44
+ void Promise.resolve().then(() => __importStar(require('./index.js')));
45
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const vitest_1 = require("vitest");
13
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
14
+ const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
15
+ const inMemory_js_1 = require("@modelcontextprotocol/sdk/inMemory.js");
16
+ const config_1 = require("./config");
17
+ const tools_1 = require("./tools");
18
+ const shared_1 = require("./tools/shared");
19
+ const hasCredentials = !!process.env.LIGHTDASH_API_KEY && !!process.env.LIGHTDASH_URL;
20
+ vitest_1.describe.runIf(hasCredentials)('MCP Integration (Real API)', () => {
21
+ (0, vitest_1.it)('should authenticate and fetch current organization', () => __awaiter(void 0, void 0, void 0, function* () {
22
+ const client = (0, config_1.getClient)();
23
+ // getCurrentOrganization is a better test for connectivity
24
+ const org = yield client.v1.organizations.getCurrentOrganization();
25
+ (0, vitest_1.expect)(org).toBeDefined();
26
+ (0, vitest_1.expect)(org.organizationUuid).toBeDefined();
27
+ (0, vitest_1.expect)(org.name).toBeDefined();
28
+ console.error(`Authenticated to organization: ${org.name}`);
29
+ }));
30
+ (0, vitest_1.it)('should execute list_projects tool with real API', () => __awaiter(void 0, void 0, void 0, function* () {
31
+ const client = (0, config_1.getClient)();
32
+ const server = new mcp_js_1.McpServer({ name: 'test-server', version: '1.0.0' });
33
+ (0, tools_1.registerTools)(server, client);
34
+ const [serverTransport, clientTransport] = inMemory_js_1.InMemoryTransport.createLinkedPair();
35
+ yield server.connect(serverTransport);
36
+ const mcpClient = new index_js_1.Client({ name: 'test-client', version: '1.0.0' }, { capabilities: {} });
37
+ yield mcpClient.connect(clientTransport);
38
+ // Call the tool through the MCP client
39
+ const result = yield mcpClient.callTool({
40
+ name: shared_1.TOOL_PREFIX + 'list_projects',
41
+ arguments: {},
42
+ });
43
+ if (result.isError) {
44
+ console.error('Tool execution failed:', result.content);
45
+ }
46
+ (0, vitest_1.expect)(result).toBeDefined();
47
+ (0, vitest_1.expect)(result.isError).toBeFalsy();
48
+ (0, vitest_1.expect)(Array.isArray(result.content)).toBe(true);
49
+ const content = result.content;
50
+ const textContent = content[0];
51
+ if (textContent && 'text' in textContent) {
52
+ (0, vitest_1.expect)(typeof textContent.text).toBe('string');
53
+ console.error(`Tool list_projects output: ${textContent.text.slice(0, 100)}...`);
54
+ }
55
+ yield mcpClient.close();
56
+ yield server.close();
57
+ }));
58
+ });
@@ -10,6 +10,7 @@ export type TextContent = {
10
10
  type: 'text';
11
11
  text: string;
12
12
  }>;
13
+ isError?: boolean;
13
14
  };
14
15
  /** Tool handler type used to avoid deep instantiation with SDK/Zod. Accepts (args, extra) for SDK compatibility. */
15
16
  export type ToolHandler = (args: unknown, extra?: unknown) => Promise<TextContent>;
@@ -62,7 +62,7 @@ function wrapTool(client, fn) {
62
62
  }
63
63
  catch (err) {
64
64
  const text = (0, errors_js_1.toMcpErrorMessage)(err);
65
- return { content: [{ type: 'text', text }] };
65
+ return { content: [{ type: 'text', text }], isError: true };
66
66
  }
67
67
  });
68
68
  }
package/package.json CHANGED
@@ -1,16 +1,19 @@
1
1
  {
2
2
  "name": "@lightdash-tools/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "MCP server and utilities for Lightdash AI.",
5
5
  "keywords": [],
6
- "license": "ISC",
6
+ "license": "Apache-2.0",
7
7
  "author": "",
8
8
  "main": "dist/index.js",
9
9
  "types": "dist/index.d.ts",
10
+ "bin": {
11
+ "lightdash-mcp": "./dist/bin.js"
12
+ },
10
13
  "dependencies": {
11
14
  "@modelcontextprotocol/sdk": "^1.26.0",
12
15
  "zod": "^4.3.6",
13
- "@lightdash-tools/client": "0.1.0"
16
+ "@lightdash-tools/client": "0.1.2"
14
17
  },
15
18
  "devDependencies": {
16
19
  "@types/node": "^25.2.3"
package/src/bin.ts ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP server CLI entrypoint.
4
+ */
5
+
6
+ const args = process.argv.slice(2);
7
+
8
+ if (args.includes('--http')) {
9
+ void import('./http.js');
10
+ } else {
11
+ void import('./index.js');
12
+ }
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
4
+ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
5
+ import { getClient } from './config';
6
+ import { registerTools } from './tools';
7
+ import { TOOL_PREFIX } from './tools/shared';
8
+
9
+ const hasCredentials = !!process.env.LIGHTDASH_API_KEY && !!process.env.LIGHTDASH_URL;
10
+
11
+ describe.runIf(hasCredentials)('MCP Integration (Real API)', () => {
12
+ it('should authenticate and fetch current organization', async () => {
13
+ const client = getClient();
14
+ // getCurrentOrganization is a better test for connectivity
15
+ const org = await client.v1.organizations.getCurrentOrganization();
16
+ expect(org).toBeDefined();
17
+ expect(org.organizationUuid).toBeDefined();
18
+ expect(org.name).toBeDefined();
19
+ console.error(`Authenticated to organization: ${org.name}`);
20
+ });
21
+
22
+ it('should execute list_projects tool with real API', async () => {
23
+ const client = getClient();
24
+ const server = new McpServer({ name: 'test-server', version: '1.0.0' });
25
+ registerTools(server, client);
26
+
27
+ const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
28
+
29
+ await server.connect(serverTransport);
30
+
31
+ const mcpClient = new Client({ name: 'test-client', version: '1.0.0' }, { capabilities: {} });
32
+ await mcpClient.connect(clientTransport);
33
+
34
+ // Call the tool through the MCP client
35
+ const result = await mcpClient.callTool({
36
+ name: TOOL_PREFIX + 'list_projects',
37
+ arguments: {},
38
+ });
39
+
40
+ if (result.isError) {
41
+ console.error('Tool execution failed:', result.content);
42
+ }
43
+ expect(result).toBeDefined();
44
+ expect(result.isError).toBeFalsy();
45
+ expect(Array.isArray(result.content)).toBe(true);
46
+ const content = result.content as { text: string }[];
47
+
48
+ const textContent = content[0];
49
+ if (textContent && 'text' in textContent) {
50
+ expect(typeof textContent.text).toBe('string');
51
+ console.error(`Tool list_projects output: ${textContent.text.slice(0, 100)}...`);
52
+ }
53
+
54
+ await mcpClient.close();
55
+ await server.close();
56
+ });
57
+ });
@@ -11,6 +11,7 @@ export const TOOL_PREFIX = 'lightdash_tools__';
11
11
 
12
12
  export type TextContent = {
13
13
  content: Array<{ type: 'text'; text: string }>;
14
+ isError?: boolean;
14
15
  };
15
16
 
16
17
  /** Tool handler type used to avoid deep instantiation with SDK/Zod. Accepts (args, extra) for SDK compatibility. */
@@ -95,7 +96,7 @@ export function wrapTool<T>(
95
96
  return await handler(args as T);
96
97
  } catch (err) {
97
98
  const text = toMcpErrorMessage(err);
98
- return { content: [{ type: 'text', text }] };
99
+ return { content: [{ type: 'text', text }], isError: true };
99
100
  }
100
101
  };
101
102
  }