@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 +50 -8
- package/dist/bin.d.ts +5 -0
- package/dist/bin.js +45 -0
- package/dist/index.integration.test.d.ts +1 -0
- package/dist/index.integration.test.js +58 -0
- package/dist/tools/shared.d.ts +1 -0
- package/dist/tools/shared.js +1 -1
- package/package.json +6 -3
- package/src/bin.ts +12 -0
- package/src/index.integration.test.ts +57 -0
- package/src/tools/shared.ts +2 -1
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
|
-
|
|
29
|
-
pnpm start
|
|
30
|
-
# or: node dist/index.js
|
|
50
|
+
lightdash-mcp
|
|
31
51
|
```
|
|
32
52
|
|
|
33
|
-
|
|
53
|
+
Logging goes to stderr only; stdout is JSON-RPC.
|
|
34
54
|
|
|
35
55
|
### Streamable HTTP (remote)
|
|
36
56
|
|
|
37
57
|
```bash
|
|
38
|
-
|
|
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
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
|
+
});
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -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>;
|
package/dist/tools/shared.js
CHANGED
package/package.json
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightdash-tools/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "MCP server and utilities for Lightdash AI.",
|
|
5
5
|
"keywords": [],
|
|
6
|
-
"license": "
|
|
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.
|
|
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,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
|
+
});
|
package/src/tools/shared.ts
CHANGED
|
@@ -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
|
}
|