@lightdash-tools/mcp 0.1.1 → 0.2.1
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 +22 -2
- package/dist/bin.js +2 -2
- package/dist/index.integration.test.d.ts +1 -0
- package/dist/index.integration.test.js +58 -0
- package/dist/tools/content.d.ts +6 -0
- package/dist/tools/content.js +37 -0
- package/dist/tools/explores.js +26 -1
- package/dist/tools/index.js +8 -0
- package/dist/tools/metrics.d.ts +6 -0
- package/dist/tools/metrics.js +45 -0
- package/dist/tools/projects.js +21 -0
- package/dist/tools/schedulers.d.ts +6 -0
- package/dist/tools/schedulers.js +45 -0
- package/dist/tools/shared.d.ts +3 -14
- package/dist/tools/shared.js +29 -25
- package/dist/tools/shared.test.d.ts +1 -0
- package/dist/tools/shared.test.js +59 -0
- package/dist/tools/tags.d.ts +6 -0
- package/dist/tools/tags.js +28 -0
- package/package.json +3 -2
- package/src/bin.ts +2 -2
- package/src/index.integration.test.ts +57 -0
- package/src/tools/content.ts +44 -0
- package/src/tools/explores.ts +53 -1
- package/src/tools/index.ts +8 -0
- package/src/tools/metrics.ts +42 -0
- package/src/tools/projects.ts +36 -1
- package/src/tools/schedulers.ts +42 -0
- package/src/tools/shared.test.ts +80 -0
- package/src/tools/shared.ts +29 -34
- package/src/tools/tags.ts +25 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @lightdash-tools/mcp
|
|
1
|
+
# [@lightdash-tools/mcp](https://www.npmjs.com/package/@lightdash-tools/mcp) <!-- markdown-link-check-disable-line -->
|
|
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
|
|
|
@@ -64,12 +64,32 @@ With auth disabled (default), any client can call the endpoint. With `MCP_AUTH_E
|
|
|
64
64
|
|
|
65
65
|
## Tools
|
|
66
66
|
|
|
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`.
|
|
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`, `list_metrics`, `list_schedulers`, `list_tags`, `search_content`.
|
|
68
68
|
|
|
69
69
|
### Destructive tools
|
|
70
70
|
|
|
71
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
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
|
+
|
|
73
93
|
## License
|
|
74
94
|
|
|
75
95
|
Apache-2.0
|
package/dist/bin.js
CHANGED
|
@@ -38,8 +38,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
38
|
})();
|
|
39
39
|
const args = process.argv.slice(2);
|
|
40
40
|
if (args.includes('--http')) {
|
|
41
|
-
Promise.resolve().then(() => __importStar(require('./http.js')));
|
|
41
|
+
void Promise.resolve().then(() => __importStar(require('./http.js')));
|
|
42
42
|
}
|
|
43
43
|
else {
|
|
44
|
-
Promise.resolve().then(() => __importStar(require('./index.js')));
|
|
44
|
+
void Promise.resolve().then(() => __importStar(require('./index.js')));
|
|
45
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
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tools: content (search).
|
|
3
|
+
*/
|
|
4
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
|
+
import type { LightdashClient } from '@lightdash-tools/client';
|
|
6
|
+
export declare function registerContentTools(server: McpServer, client: LightdashClient): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* MCP tools: content (search).
|
|
4
|
+
*/
|
|
5
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
6
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
7
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
8
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
9
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
10
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
11
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.registerContentTools = registerContentTools;
|
|
16
|
+
const zod_1 = require("zod");
|
|
17
|
+
const shared_js_1 = require("./shared.js");
|
|
18
|
+
function registerContentTools(server, client) {
|
|
19
|
+
(0, shared_js_1.registerToolSafe)(server, 'search_content', {
|
|
20
|
+
title: 'Search content',
|
|
21
|
+
description: 'Search for charts, dashboards, and spaces across projects',
|
|
22
|
+
inputSchema: {
|
|
23
|
+
search: zod_1.z.string().describe('Search query'),
|
|
24
|
+
projectUuids: zod_1.z.array(zod_1.z.string()).optional().describe('Optional project UUIDs to filter'),
|
|
25
|
+
contentTypes: zod_1.z
|
|
26
|
+
.array(zod_1.z.enum(['chart', 'dashboard', 'space']))
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('Optional content types to filter'),
|
|
29
|
+
page: zod_1.z.number().optional().describe('Page number'),
|
|
30
|
+
pageSize: zod_1.z.number().optional().describe('Page size'),
|
|
31
|
+
},
|
|
32
|
+
annotations: shared_js_1.READ_ONLY_DEFAULT,
|
|
33
|
+
}, (0, shared_js_1.wrapTool)(client, (c) => (params) => __awaiter(this, void 0, void 0, function* () {
|
|
34
|
+
const result = yield c.v2.content.searchContent(params);
|
|
35
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
36
|
+
})));
|
|
37
|
+
}
|
package/dist/tools/explores.js
CHANGED
|
@@ -27,7 +27,7 @@ function registerExploresTools(server, client) {
|
|
|
27
27
|
})));
|
|
28
28
|
(0, shared_js_1.registerToolSafe)(server, 'get_explore', {
|
|
29
29
|
title: 'Get explore',
|
|
30
|
-
description: 'Get an explore by project UUID and explore ID',
|
|
30
|
+
description: 'Get an explore by project UUID and explore ID (includes tables, dimensions, and metrics)',
|
|
31
31
|
inputSchema: {
|
|
32
32
|
projectUuid: zod_1.z.string().describe('Project UUID'),
|
|
33
33
|
exploreId: zod_1.z.string().describe('Explore ID'),
|
|
@@ -37,4 +37,29 @@ function registerExploresTools(server, client) {
|
|
|
37
37
|
const explore = yield c.v1.explores.getExplore(projectUuid, exploreId);
|
|
38
38
|
return { content: [{ type: 'text', text: JSON.stringify(explore, null, 2) }] };
|
|
39
39
|
})));
|
|
40
|
+
(0, shared_js_1.registerToolSafe)(server, 'list_dimensions', {
|
|
41
|
+
title: 'List dimensions',
|
|
42
|
+
description: 'List all dimensions for a specific explore',
|
|
43
|
+
inputSchema: {
|
|
44
|
+
projectUuid: zod_1.z.string().describe('Project UUID'),
|
|
45
|
+
exploreId: zod_1.z.string().describe('Explore ID'),
|
|
46
|
+
},
|
|
47
|
+
annotations: shared_js_1.READ_ONLY_DEFAULT,
|
|
48
|
+
}, (0, shared_js_1.wrapTool)(client, (c) => (_a) => __awaiter(this, [_a], void 0, function* ({ projectUuid, exploreId }) {
|
|
49
|
+
const result = yield c.v1.explores.listDimensions(projectUuid, exploreId);
|
|
50
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
51
|
+
})));
|
|
52
|
+
(0, shared_js_1.registerToolSafe)(server, 'get_field_lineage', {
|
|
53
|
+
title: 'Get field lineage',
|
|
54
|
+
description: 'Get upstream lineage for a specific field in an explore',
|
|
55
|
+
inputSchema: {
|
|
56
|
+
projectUuid: zod_1.z.string().describe('Project UUID'),
|
|
57
|
+
exploreId: zod_1.z.string().describe('Explore ID'),
|
|
58
|
+
fieldId: zod_1.z.string().describe('Field ID'),
|
|
59
|
+
},
|
|
60
|
+
annotations: shared_js_1.READ_ONLY_DEFAULT,
|
|
61
|
+
}, (0, shared_js_1.wrapTool)(client, (c) => (_a) => __awaiter(this, [_a], void 0, function* ({ projectUuid, exploreId, fieldId, }) {
|
|
62
|
+
const result = yield c.v1.explores.getFieldLineage(projectUuid, exploreId, fieldId);
|
|
63
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
64
|
+
})));
|
|
40
65
|
}
|
package/dist/tools/index.js
CHANGED
|
@@ -12,6 +12,10 @@ const users_js_1 = require("./users.js");
|
|
|
12
12
|
const groups_js_1 = require("./groups.js");
|
|
13
13
|
const query_js_1 = require("./query.js");
|
|
14
14
|
const explores_js_1 = require("./explores.js");
|
|
15
|
+
const metrics_js_1 = require("./metrics.js");
|
|
16
|
+
const schedulers_js_1 = require("./schedulers.js");
|
|
17
|
+
const tags_js_1 = require("./tags.js");
|
|
18
|
+
const content_js_1 = require("./content.js");
|
|
15
19
|
function registerTools(server, client) {
|
|
16
20
|
(0, projects_js_1.registerProjectTools)(server, client);
|
|
17
21
|
(0, charts_js_1.registerChartTools)(server, client);
|
|
@@ -21,4 +25,8 @@ function registerTools(server, client) {
|
|
|
21
25
|
(0, groups_js_1.registerGroupTools)(server, client);
|
|
22
26
|
(0, query_js_1.registerQueryTools)(server, client);
|
|
23
27
|
(0, explores_js_1.registerExploresTools)(server, client);
|
|
28
|
+
(0, metrics_js_1.registerMetricsTools)(server, client);
|
|
29
|
+
(0, schedulers_js_1.registerSchedulersTools)(server, client);
|
|
30
|
+
(0, tags_js_1.registerTagsTools)(server, client);
|
|
31
|
+
(0, content_js_1.registerContentTools)(server, client);
|
|
24
32
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tools: metrics (list, get).
|
|
3
|
+
*/
|
|
4
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
|
+
import type { LightdashClient } from '@lightdash-tools/client';
|
|
6
|
+
export declare function registerMetricsTools(server: McpServer, client: LightdashClient): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* MCP tools: metrics (list, get).
|
|
4
|
+
*/
|
|
5
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
6
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
7
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
8
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
9
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
10
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
11
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
15
|
+
var t = {};
|
|
16
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
17
|
+
t[p] = s[p];
|
|
18
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
19
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
20
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
21
|
+
t[p[i]] = s[p[i]];
|
|
22
|
+
}
|
|
23
|
+
return t;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.registerMetricsTools = registerMetricsTools;
|
|
27
|
+
const zod_1 = require("zod");
|
|
28
|
+
const shared_js_1 = require("./shared.js");
|
|
29
|
+
function registerMetricsTools(server, client) {
|
|
30
|
+
(0, shared_js_1.registerToolSafe)(server, 'list_metrics', {
|
|
31
|
+
title: 'List metrics',
|
|
32
|
+
description: 'List metrics in a project data catalog',
|
|
33
|
+
inputSchema: {
|
|
34
|
+
projectUuid: zod_1.z.string().describe('Project UUID'),
|
|
35
|
+
search: zod_1.z.string().optional().describe('Search query'),
|
|
36
|
+
page: zod_1.z.number().optional().describe('Page number'),
|
|
37
|
+
pageSize: zod_1.z.number().optional().describe('Page size'),
|
|
38
|
+
},
|
|
39
|
+
annotations: shared_js_1.READ_ONLY_DEFAULT,
|
|
40
|
+
}, (0, shared_js_1.wrapTool)(client, (c) => (_a) => __awaiter(this, void 0, void 0, function* () {
|
|
41
|
+
var { projectUuid } = _a, params = __rest(_a, ["projectUuid"]);
|
|
42
|
+
const result = yield c.v1.metrics.listMetrics(projectUuid, params);
|
|
43
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
44
|
+
})));
|
|
45
|
+
}
|
package/dist/tools/projects.js
CHANGED
|
@@ -34,4 +34,25 @@ function registerProjectTools(server, client) {
|
|
|
34
34
|
const project = yield c.v1.projects.getProject(projectUuid);
|
|
35
35
|
return { content: [{ type: 'text', text: JSON.stringify(project, null, 2) }] };
|
|
36
36
|
})));
|
|
37
|
+
(0, shared_js_1.registerToolSafe)(server, 'validate_project', {
|
|
38
|
+
title: 'Validate project',
|
|
39
|
+
description: 'Trigger a validation job for a project and return the job ID',
|
|
40
|
+
inputSchema: { projectUuid: zod_1.z.string().describe('Project UUID') },
|
|
41
|
+
annotations: shared_js_1.WRITE_IDEMPOTENT,
|
|
42
|
+
}, (0, shared_js_1.wrapTool)(client, (c) => (_a) => __awaiter(this, [_a], void 0, function* ({ projectUuid }) {
|
|
43
|
+
const result = yield c.v1.validation.validateProject(projectUuid);
|
|
44
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
45
|
+
})));
|
|
46
|
+
(0, shared_js_1.registerToolSafe)(server, 'get_validation_results', {
|
|
47
|
+
title: 'Get validation results',
|
|
48
|
+
description: 'Get the latest validation results for a project',
|
|
49
|
+
inputSchema: {
|
|
50
|
+
projectUuid: zod_1.z.string().describe('Project UUID'),
|
|
51
|
+
jobId: zod_1.z.string().optional().describe('Optional job ID to get results for'),
|
|
52
|
+
},
|
|
53
|
+
annotations: shared_js_1.READ_ONLY_DEFAULT,
|
|
54
|
+
}, (0, shared_js_1.wrapTool)(client, (c) => (_a) => __awaiter(this, [_a], void 0, function* ({ projectUuid, jobId }) {
|
|
55
|
+
const result = yield c.v1.validation.getValidationResults(projectUuid, { jobId });
|
|
56
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
57
|
+
})));
|
|
37
58
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tools: schedulers (list, get).
|
|
3
|
+
*/
|
|
4
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
|
+
import type { LightdashClient } from '@lightdash-tools/client';
|
|
6
|
+
export declare function registerSchedulersTools(server: McpServer, client: LightdashClient): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* MCP tools: schedulers (list, get).
|
|
4
|
+
*/
|
|
5
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
6
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
7
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
8
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
9
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
10
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
11
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
15
|
+
var t = {};
|
|
16
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
17
|
+
t[p] = s[p];
|
|
18
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
19
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
20
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
21
|
+
t[p[i]] = s[p[i]];
|
|
22
|
+
}
|
|
23
|
+
return t;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.registerSchedulersTools = registerSchedulersTools;
|
|
27
|
+
const zod_1 = require("zod");
|
|
28
|
+
const shared_js_1 = require("./shared.js");
|
|
29
|
+
function registerSchedulersTools(server, client) {
|
|
30
|
+
(0, shared_js_1.registerToolSafe)(server, 'list_schedulers', {
|
|
31
|
+
title: 'List schedulers',
|
|
32
|
+
description: 'List scheduled deliveries in a project',
|
|
33
|
+
inputSchema: {
|
|
34
|
+
projectUuid: zod_1.z.string().describe('Project UUID'),
|
|
35
|
+
searchQuery: zod_1.z.string().optional().describe('Search query'),
|
|
36
|
+
page: zod_1.z.number().optional().describe('Page number'),
|
|
37
|
+
pageSize: zod_1.z.number().optional().describe('Page size'),
|
|
38
|
+
},
|
|
39
|
+
annotations: shared_js_1.READ_ONLY_DEFAULT,
|
|
40
|
+
}, (0, shared_js_1.wrapTool)(client, (c) => (_a) => __awaiter(this, void 0, void 0, function* () {
|
|
41
|
+
var { projectUuid } = _a, params = __rest(_a, ["projectUuid"]);
|
|
42
|
+
const result = yield c.v1.schedulers.listSchedulers(projectUuid, params);
|
|
43
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
44
|
+
})));
|
|
45
|
+
}
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Shared types and helpers for MCP tool registration.
|
|
3
3
|
*/
|
|
4
4
|
import type { LightdashClient } from '@lightdash-tools/client';
|
|
5
|
+
import type { ToolAnnotations } from '@lightdash-tools/common';
|
|
5
6
|
import type { z } from 'zod';
|
|
6
7
|
/** Prefix for all MCP tool names (disambiguation when multiple servers are connected). */
|
|
7
8
|
export declare const TOOL_PREFIX = "lightdash_tools__";
|
|
@@ -10,17 +11,10 @@ export type TextContent = {
|
|
|
10
11
|
type: 'text';
|
|
11
12
|
text: string;
|
|
12
13
|
}>;
|
|
14
|
+
isError?: boolean;
|
|
13
15
|
};
|
|
14
16
|
/** Tool handler type used to avoid deep instantiation with SDK/Zod. Accepts (args, extra) for SDK compatibility. */
|
|
15
17
|
export type ToolHandler = (args: unknown, extra?: unknown) => Promise<TextContent>;
|
|
16
|
-
/** MCP tool annotations (hints for client display and approval). See MCP spec Tool annotations. */
|
|
17
|
-
export type ToolAnnotations = {
|
|
18
|
-
title?: string;
|
|
19
|
-
readOnlyHint?: boolean;
|
|
20
|
-
destructiveHint?: boolean;
|
|
21
|
-
idempotentHint?: boolean;
|
|
22
|
-
openWorldHint?: boolean;
|
|
23
|
-
};
|
|
24
18
|
/** Options for registerTool; inputSchema typed as ZodRawShapeCompat for SDK compatibility. Pass annotations explicitly (e.g. READ_ONLY_DEFAULT or WRITE_IDEMPOTENT) for visibility. */
|
|
25
19
|
export type ToolOptions = {
|
|
26
20
|
description: string;
|
|
@@ -28,12 +22,7 @@ export type ToolOptions = {
|
|
|
28
22
|
title?: string;
|
|
29
23
|
annotations?: ToolAnnotations;
|
|
30
24
|
};
|
|
31
|
-
|
|
32
|
-
export declare const READ_ONLY_DEFAULT: ToolAnnotations;
|
|
33
|
-
/** Preset: write, non-destructive, idempotent (e.g. upsert by slug). Use for create/update tools. */
|
|
34
|
-
export declare const WRITE_IDEMPOTENT: ToolAnnotations;
|
|
35
|
-
/** Preset: write, destructive, non-idempotent. Use for delete/remove tools; clients should prompt for user confirmation. */
|
|
36
|
-
export declare const WRITE_DESTRUCTIVE: ToolAnnotations;
|
|
25
|
+
export { READ_ONLY_DEFAULT, WRITE_IDEMPOTENT, WRITE_DESTRUCTIVE } from '@lightdash-tools/common';
|
|
37
26
|
/** Registers a tool with prefix and annotations. shortName is TOOL_PREFIX + shortName. Pass annotations explicitly (e.g. READ_ONLY_DEFAULT, WRITE_IDEMPOTENT, or WRITE_DESTRUCTIVE). */
|
|
38
27
|
export declare function registerToolSafe(server: unknown, shortName: string, options: ToolOptions, handler: ToolHandler): void;
|
|
39
28
|
export declare function wrapTool<T>(client: LightdashClient, fn: (client: LightdashClient) => (args: T) => Promise<TextContent>): ToolHandler;
|
package/dist/tools/shared.js
CHANGED
|
@@ -15,32 +15,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
exports.WRITE_DESTRUCTIVE = exports.WRITE_IDEMPOTENT = exports.READ_ONLY_DEFAULT = exports.TOOL_PREFIX = void 0;
|
|
16
16
|
exports.registerToolSafe = registerToolSafe;
|
|
17
17
|
exports.wrapTool = wrapTool;
|
|
18
|
+
const common_1 = require("@lightdash-tools/common");
|
|
18
19
|
const errors_js_1 = require("../errors.js");
|
|
19
20
|
/** Prefix for all MCP tool names (disambiguation when multiple servers are connected). */
|
|
20
21
|
exports.TOOL_PREFIX = 'lightdash_tools__';
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
idempotentHint: true,
|
|
27
|
-
};
|
|
28
|
-
/** Preset: write, non-destructive, idempotent (e.g. upsert by slug). Use for create/update tools. */
|
|
29
|
-
exports.WRITE_IDEMPOTENT = {
|
|
30
|
-
readOnlyHint: false,
|
|
31
|
-
openWorldHint: false,
|
|
32
|
-
destructiveHint: false,
|
|
33
|
-
idempotentHint: true,
|
|
34
|
-
};
|
|
35
|
-
/** Preset: write, destructive, non-idempotent. Use for delete/remove tools; clients should prompt for user confirmation. */
|
|
36
|
-
exports.WRITE_DESTRUCTIVE = {
|
|
37
|
-
readOnlyHint: false,
|
|
38
|
-
openWorldHint: false,
|
|
39
|
-
destructiveHint: true,
|
|
40
|
-
idempotentHint: false,
|
|
41
|
-
};
|
|
22
|
+
// Re-export presets for convenience and backward compatibility in tools
|
|
23
|
+
var common_2 = require("@lightdash-tools/common");
|
|
24
|
+
Object.defineProperty(exports, "READ_ONLY_DEFAULT", { enumerable: true, get: function () { return common_2.READ_ONLY_DEFAULT; } });
|
|
25
|
+
Object.defineProperty(exports, "WRITE_IDEMPOTENT", { enumerable: true, get: function () { return common_2.WRITE_IDEMPOTENT; } });
|
|
26
|
+
Object.defineProperty(exports, "WRITE_DESTRUCTIVE", { enumerable: true, get: function () { return common_2.WRITE_DESTRUCTIVE; } });
|
|
42
27
|
/** Internal default for mergeAnnotations; READ_ONLY_DEFAULT is the exported preset. */
|
|
43
|
-
const DEFAULT_ANNOTATIONS =
|
|
28
|
+
const DEFAULT_ANNOTATIONS = common_1.READ_ONLY_DEFAULT;
|
|
44
29
|
/** Merges per-tool annotations with defaults; per-tool values win. */
|
|
45
30
|
function mergeAnnotations(overrides) {
|
|
46
31
|
return Object.assign(Object.assign({}, DEFAULT_ANNOTATIONS), overrides);
|
|
@@ -50,8 +35,27 @@ function registerToolSafe(server, shortName, options, handler) {
|
|
|
50
35
|
var _a, _b;
|
|
51
36
|
const name = exports.TOOL_PREFIX + shortName;
|
|
52
37
|
const annotations = mergeAnnotations(options.annotations);
|
|
53
|
-
const
|
|
54
|
-
|
|
38
|
+
const mode = (0, common_1.getSafetyModeFromEnv)();
|
|
39
|
+
const isToolAllowed = (0, common_1.isAllowed)(mode, annotations);
|
|
40
|
+
// If not allowed, wrap handler to return an error and update description
|
|
41
|
+
let finalHandler = handler;
|
|
42
|
+
let finalDescription = options.description;
|
|
43
|
+
if (!isToolAllowed) {
|
|
44
|
+
finalDescription = `[DISABLED in ${mode} mode] ${options.description}`;
|
|
45
|
+
finalHandler = () => __awaiter(this, void 0, void 0, function* () {
|
|
46
|
+
return ({
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: 'text',
|
|
50
|
+
text: `Error: Tool '${name}' is disabled in ${mode} mode. To enable it, change LIGHTDASH_AI_MODE.`,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
isError: true,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const mergedOptions = Object.assign(Object.assign({}, options), { description: finalDescription, title: (_a = options.title) !== null && _a !== void 0 ? _a : (_b = options.annotations) === null || _b === void 0 ? void 0 : _b.title, annotations });
|
|
58
|
+
server.registerTool(name, mergedOptions, finalHandler);
|
|
55
59
|
}
|
|
56
60
|
function wrapTool(client, fn) {
|
|
57
61
|
const handler = fn(client);
|
|
@@ -62,7 +66,7 @@ function wrapTool(client, fn) {
|
|
|
62
66
|
}
|
|
63
67
|
catch (err) {
|
|
64
68
|
const text = (0, errors_js_1.toMcpErrorMessage)(err);
|
|
65
|
-
return { content: [{ type: 'text', text }] };
|
|
69
|
+
return { content: [{ type: 'text', text }], isError: true };
|
|
66
70
|
}
|
|
67
71
|
});
|
|
68
72
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
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 shared_1 = require("./shared");
|
|
14
|
+
const common_1 = require("@lightdash-tools/common");
|
|
15
|
+
(0, vitest_1.describe)('registerToolSafe', () => {
|
|
16
|
+
const mockServer = {
|
|
17
|
+
registerTool: vitest_1.vi.fn(),
|
|
18
|
+
};
|
|
19
|
+
const mockHandler = vitest_1.vi.fn().mockResolvedValue({ content: [{ type: 'text', text: 'success' }] });
|
|
20
|
+
(0, vitest_1.it)('should allow read-only tool in read-only mode', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
21
|
+
process.env.LIGHTDASH_AI_MODE = common_1.SafetyMode.READ_ONLY;
|
|
22
|
+
(0, shared_1.registerToolSafe)(mockServer, 'test_tool', {
|
|
23
|
+
description: 'Test description',
|
|
24
|
+
inputSchema: {},
|
|
25
|
+
annotations: shared_1.READ_ONLY_DEFAULT,
|
|
26
|
+
}, mockHandler);
|
|
27
|
+
(0, vitest_1.expect)(mockServer.registerTool).toHaveBeenCalled();
|
|
28
|
+
const [name, options, handler] = mockServer.registerTool.mock.calls[0];
|
|
29
|
+
(0, vitest_1.expect)(name).toContain('test_tool');
|
|
30
|
+
(0, vitest_1.expect)(options.description).toBe('Test description');
|
|
31
|
+
const result = yield handler({});
|
|
32
|
+
(0, vitest_1.expect)(result.content[0].text).toBe('success');
|
|
33
|
+
}));
|
|
34
|
+
(0, vitest_1.it)('should block destructive tool in read-only mode', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
35
|
+
process.env.LIGHTDASH_AI_MODE = common_1.SafetyMode.READ_ONLY;
|
|
36
|
+
(0, shared_1.registerToolSafe)(mockServer, 'delete_tool', {
|
|
37
|
+
description: 'Delete something',
|
|
38
|
+
inputSchema: {},
|
|
39
|
+
annotations: shared_1.WRITE_DESTRUCTIVE,
|
|
40
|
+
}, mockHandler);
|
|
41
|
+
const [, options, handler] = mockServer.registerTool.mock.calls[1];
|
|
42
|
+
(0, vitest_1.expect)(options.description).toContain('[DISABLED in read-only mode]');
|
|
43
|
+
const result = yield handler({});
|
|
44
|
+
(0, vitest_1.expect)(result.isError).toBe(true);
|
|
45
|
+
(0, vitest_1.expect)(result.content[0].text).toContain('disabled in read-only mode');
|
|
46
|
+
}));
|
|
47
|
+
(0, vitest_1.it)('should allow destructive tool in write-destructive mode', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
48
|
+
process.env.LIGHTDASH_AI_MODE = common_1.SafetyMode.WRITE_DESTRUCTIVE;
|
|
49
|
+
(0, shared_1.registerToolSafe)(mockServer, 'delete_tool_2', {
|
|
50
|
+
description: 'Delete something 2',
|
|
51
|
+
inputSchema: {},
|
|
52
|
+
annotations: shared_1.WRITE_DESTRUCTIVE,
|
|
53
|
+
}, mockHandler);
|
|
54
|
+
const [, options, handler] = mockServer.registerTool.mock.calls[2];
|
|
55
|
+
(0, vitest_1.expect)(options.description).toBe('Delete something 2');
|
|
56
|
+
const result = yield handler({});
|
|
57
|
+
(0, vitest_1.expect)(result.content[0].text).toBe('success');
|
|
58
|
+
}));
|
|
59
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tools: tags (list).
|
|
3
|
+
*/
|
|
4
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
|
+
import type { LightdashClient } from '@lightdash-tools/client';
|
|
6
|
+
export declare function registerTagsTools(server: McpServer, client: LightdashClient): void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* MCP tools: tags (list).
|
|
4
|
+
*/
|
|
5
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
6
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
7
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
8
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
9
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
10
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
11
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.registerTagsTools = registerTagsTools;
|
|
16
|
+
const zod_1 = require("zod");
|
|
17
|
+
const shared_js_1 = require("./shared.js");
|
|
18
|
+
function registerTagsTools(server, client) {
|
|
19
|
+
(0, shared_js_1.registerToolSafe)(server, 'list_tags', {
|
|
20
|
+
title: 'List tags',
|
|
21
|
+
description: 'List all tags in a project',
|
|
22
|
+
inputSchema: { projectUuid: zod_1.z.string().describe('Project UUID') },
|
|
23
|
+
annotations: shared_js_1.READ_ONLY_DEFAULT,
|
|
24
|
+
}, (0, shared_js_1.wrapTool)(client, (c) => (_a) => __awaiter(this, [_a], void 0, function* ({ projectUuid }) {
|
|
25
|
+
const result = yield c.v1.tags.listTags(projectUuid);
|
|
26
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
27
|
+
})));
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightdash-tools/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "MCP server and utilities for Lightdash AI.",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
15
15
|
"zod": "^4.3.6",
|
|
16
|
-
"@lightdash-tools/client": "0.
|
|
16
|
+
"@lightdash-tools/client": "0.2.1",
|
|
17
|
+
"@lightdash-tools/common": "0.2.1"
|
|
17
18
|
},
|
|
18
19
|
"devDependencies": {
|
|
19
20
|
"@types/node": "^25.2.3"
|
package/src/bin.ts
CHANGED
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tools: content (search).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
+
import type { LightdashClient } from '@lightdash-tools/client';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { wrapTool, registerToolSafe, READ_ONLY_DEFAULT } from './shared.js';
|
|
9
|
+
|
|
10
|
+
export function registerContentTools(server: McpServer, client: LightdashClient): void {
|
|
11
|
+
registerToolSafe(
|
|
12
|
+
server,
|
|
13
|
+
'search_content',
|
|
14
|
+
{
|
|
15
|
+
title: 'Search content',
|
|
16
|
+
description: 'Search for charts, dashboards, and spaces across projects',
|
|
17
|
+
inputSchema: {
|
|
18
|
+
search: z.string().describe('Search query'),
|
|
19
|
+
projectUuids: z.array(z.string()).optional().describe('Optional project UUIDs to filter'),
|
|
20
|
+
contentTypes: z
|
|
21
|
+
.array(z.enum(['chart', 'dashboard', 'space']))
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('Optional content types to filter'),
|
|
24
|
+
page: z.number().optional().describe('Page number'),
|
|
25
|
+
pageSize: z.number().optional().describe('Page size'),
|
|
26
|
+
},
|
|
27
|
+
annotations: READ_ONLY_DEFAULT,
|
|
28
|
+
},
|
|
29
|
+
wrapTool(
|
|
30
|
+
client,
|
|
31
|
+
(c) =>
|
|
32
|
+
async (params: {
|
|
33
|
+
search: string;
|
|
34
|
+
projectUuids?: string[];
|
|
35
|
+
contentTypes?: ('chart' | 'dashboard' | 'space')[];
|
|
36
|
+
page?: number;
|
|
37
|
+
pageSize?: number;
|
|
38
|
+
}) => {
|
|
39
|
+
const result = await c.v2.content.searchContent(params);
|
|
40
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
41
|
+
},
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
}
|
package/src/tools/explores.ts
CHANGED
|
@@ -27,7 +27,8 @@ export function registerExploresTools(server: McpServer, client: LightdashClient
|
|
|
27
27
|
'get_explore',
|
|
28
28
|
{
|
|
29
29
|
title: 'Get explore',
|
|
30
|
-
description:
|
|
30
|
+
description:
|
|
31
|
+
'Get an explore by project UUID and explore ID (includes tables, dimensions, and metrics)',
|
|
31
32
|
inputSchema: {
|
|
32
33
|
projectUuid: z.string().describe('Project UUID'),
|
|
33
34
|
exploreId: z.string().describe('Explore ID'),
|
|
@@ -43,4 +44,55 @@ export function registerExploresTools(server: McpServer, client: LightdashClient
|
|
|
43
44
|
},
|
|
44
45
|
),
|
|
45
46
|
);
|
|
47
|
+
registerToolSafe(
|
|
48
|
+
server,
|
|
49
|
+
'list_dimensions',
|
|
50
|
+
{
|
|
51
|
+
title: 'List dimensions',
|
|
52
|
+
description: 'List all dimensions for a specific explore',
|
|
53
|
+
inputSchema: {
|
|
54
|
+
projectUuid: z.string().describe('Project UUID'),
|
|
55
|
+
exploreId: z.string().describe('Explore ID'),
|
|
56
|
+
},
|
|
57
|
+
annotations: READ_ONLY_DEFAULT,
|
|
58
|
+
},
|
|
59
|
+
wrapTool(
|
|
60
|
+
client,
|
|
61
|
+
(c) =>
|
|
62
|
+
async ({ projectUuid, exploreId }: { projectUuid: string; exploreId: string }) => {
|
|
63
|
+
const result = await c.v1.explores.listDimensions(projectUuid, exploreId);
|
|
64
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
65
|
+
},
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
registerToolSafe(
|
|
69
|
+
server,
|
|
70
|
+
'get_field_lineage',
|
|
71
|
+
{
|
|
72
|
+
title: 'Get field lineage',
|
|
73
|
+
description: 'Get upstream lineage for a specific field in an explore',
|
|
74
|
+
inputSchema: {
|
|
75
|
+
projectUuid: z.string().describe('Project UUID'),
|
|
76
|
+
exploreId: z.string().describe('Explore ID'),
|
|
77
|
+
fieldId: z.string().describe('Field ID'),
|
|
78
|
+
},
|
|
79
|
+
annotations: READ_ONLY_DEFAULT,
|
|
80
|
+
},
|
|
81
|
+
wrapTool(
|
|
82
|
+
client,
|
|
83
|
+
(c) =>
|
|
84
|
+
async ({
|
|
85
|
+
projectUuid,
|
|
86
|
+
exploreId,
|
|
87
|
+
fieldId,
|
|
88
|
+
}: {
|
|
89
|
+
projectUuid: string;
|
|
90
|
+
exploreId: string;
|
|
91
|
+
fieldId: string;
|
|
92
|
+
}) => {
|
|
93
|
+
const result = await c.v1.explores.getFieldLineage(projectUuid, exploreId, fieldId);
|
|
94
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
95
|
+
},
|
|
96
|
+
),
|
|
97
|
+
);
|
|
46
98
|
}
|
package/src/tools/index.ts
CHANGED
|
@@ -12,6 +12,10 @@ import { registerUserTools } from './users.js';
|
|
|
12
12
|
import { registerGroupTools } from './groups.js';
|
|
13
13
|
import { registerQueryTools } from './query.js';
|
|
14
14
|
import { registerExploresTools } from './explores.js';
|
|
15
|
+
import { registerMetricsTools } from './metrics.js';
|
|
16
|
+
import { registerSchedulersTools } from './schedulers.js';
|
|
17
|
+
import { registerTagsTools } from './tags.js';
|
|
18
|
+
import { registerContentTools } from './content.js';
|
|
15
19
|
|
|
16
20
|
export function registerTools(server: McpServer, client: LightdashClient): void {
|
|
17
21
|
registerProjectTools(server, client);
|
|
@@ -22,4 +26,8 @@ export function registerTools(server: McpServer, client: LightdashClient): void
|
|
|
22
26
|
registerGroupTools(server, client);
|
|
23
27
|
registerQueryTools(server, client);
|
|
24
28
|
registerExploresTools(server, client);
|
|
29
|
+
registerMetricsTools(server, client);
|
|
30
|
+
registerSchedulersTools(server, client);
|
|
31
|
+
registerTagsTools(server, client);
|
|
32
|
+
registerContentTools(server, client);
|
|
25
33
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tools: metrics (list, get).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
+
import type { LightdashClient } from '@lightdash-tools/client';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { wrapTool, registerToolSafe, READ_ONLY_DEFAULT } from './shared.js';
|
|
9
|
+
|
|
10
|
+
export function registerMetricsTools(server: McpServer, client: LightdashClient): void {
|
|
11
|
+
registerToolSafe(
|
|
12
|
+
server,
|
|
13
|
+
'list_metrics',
|
|
14
|
+
{
|
|
15
|
+
title: 'List metrics',
|
|
16
|
+
description: 'List metrics in a project data catalog',
|
|
17
|
+
inputSchema: {
|
|
18
|
+
projectUuid: z.string().describe('Project UUID'),
|
|
19
|
+
search: z.string().optional().describe('Search query'),
|
|
20
|
+
page: z.number().optional().describe('Page number'),
|
|
21
|
+
pageSize: z.number().optional().describe('Page size'),
|
|
22
|
+
},
|
|
23
|
+
annotations: READ_ONLY_DEFAULT,
|
|
24
|
+
},
|
|
25
|
+
wrapTool(
|
|
26
|
+
client,
|
|
27
|
+
(c) =>
|
|
28
|
+
async ({
|
|
29
|
+
projectUuid,
|
|
30
|
+
...params
|
|
31
|
+
}: {
|
|
32
|
+
projectUuid: string;
|
|
33
|
+
search?: string;
|
|
34
|
+
page?: number;
|
|
35
|
+
pageSize?: number;
|
|
36
|
+
}) => {
|
|
37
|
+
const result = await c.v1.metrics.listMetrics(projectUuid, params);
|
|
38
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
39
|
+
},
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
}
|
package/src/tools/projects.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
6
|
import type { LightdashClient } from '@lightdash-tools/client';
|
|
7
7
|
import { z } from 'zod';
|
|
8
|
-
import { wrapTool, registerToolSafe, READ_ONLY_DEFAULT } from './shared.js';
|
|
8
|
+
import { wrapTool, registerToolSafe, READ_ONLY_DEFAULT, WRITE_IDEMPOTENT } from './shared.js';
|
|
9
9
|
|
|
10
10
|
export function registerProjectTools(server: McpServer, client: LightdashClient): void {
|
|
11
11
|
registerToolSafe(
|
|
@@ -36,4 +36,39 @@ export function registerProjectTools(server: McpServer, client: LightdashClient)
|
|
|
36
36
|
return { content: [{ type: 'text', text: JSON.stringify(project, null, 2) }] };
|
|
37
37
|
}),
|
|
38
38
|
);
|
|
39
|
+
registerToolSafe(
|
|
40
|
+
server,
|
|
41
|
+
'validate_project',
|
|
42
|
+
{
|
|
43
|
+
title: 'Validate project',
|
|
44
|
+
description: 'Trigger a validation job for a project and return the job ID',
|
|
45
|
+
inputSchema: { projectUuid: z.string().describe('Project UUID') },
|
|
46
|
+
annotations: WRITE_IDEMPOTENT,
|
|
47
|
+
},
|
|
48
|
+
wrapTool(client, (c) => async ({ projectUuid }: { projectUuid: string }) => {
|
|
49
|
+
const result = await c.v1.validation.validateProject(projectUuid);
|
|
50
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
registerToolSafe(
|
|
54
|
+
server,
|
|
55
|
+
'get_validation_results',
|
|
56
|
+
{
|
|
57
|
+
title: 'Get validation results',
|
|
58
|
+
description: 'Get the latest validation results for a project',
|
|
59
|
+
inputSchema: {
|
|
60
|
+
projectUuid: z.string().describe('Project UUID'),
|
|
61
|
+
jobId: z.string().optional().describe('Optional job ID to get results for'),
|
|
62
|
+
},
|
|
63
|
+
annotations: READ_ONLY_DEFAULT,
|
|
64
|
+
},
|
|
65
|
+
wrapTool(
|
|
66
|
+
client,
|
|
67
|
+
(c) =>
|
|
68
|
+
async ({ projectUuid, jobId }: { projectUuid: string; jobId?: string }) => {
|
|
69
|
+
const result = await c.v1.validation.getValidationResults(projectUuid, { jobId });
|
|
70
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
71
|
+
},
|
|
72
|
+
),
|
|
73
|
+
);
|
|
39
74
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tools: schedulers (list, get).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
+
import type { LightdashClient } from '@lightdash-tools/client';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { wrapTool, registerToolSafe, READ_ONLY_DEFAULT } from './shared.js';
|
|
9
|
+
|
|
10
|
+
export function registerSchedulersTools(server: McpServer, client: LightdashClient): void {
|
|
11
|
+
registerToolSafe(
|
|
12
|
+
server,
|
|
13
|
+
'list_schedulers',
|
|
14
|
+
{
|
|
15
|
+
title: 'List schedulers',
|
|
16
|
+
description: 'List scheduled deliveries in a project',
|
|
17
|
+
inputSchema: {
|
|
18
|
+
projectUuid: z.string().describe('Project UUID'),
|
|
19
|
+
searchQuery: z.string().optional().describe('Search query'),
|
|
20
|
+
page: z.number().optional().describe('Page number'),
|
|
21
|
+
pageSize: z.number().optional().describe('Page size'),
|
|
22
|
+
},
|
|
23
|
+
annotations: READ_ONLY_DEFAULT,
|
|
24
|
+
},
|
|
25
|
+
wrapTool(
|
|
26
|
+
client,
|
|
27
|
+
(c) =>
|
|
28
|
+
async ({
|
|
29
|
+
projectUuid,
|
|
30
|
+
...params
|
|
31
|
+
}: {
|
|
32
|
+
projectUuid: string;
|
|
33
|
+
searchQuery?: string;
|
|
34
|
+
page?: number;
|
|
35
|
+
pageSize?: number;
|
|
36
|
+
}) => {
|
|
37
|
+
const result = await c.v1.schedulers.listSchedulers(projectUuid, params);
|
|
38
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
39
|
+
},
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { registerToolSafe, READ_ONLY_DEFAULT, WRITE_DESTRUCTIVE } from './shared';
|
|
3
|
+
import { SafetyMode } from '@lightdash-tools/common';
|
|
4
|
+
|
|
5
|
+
describe('registerToolSafe', () => {
|
|
6
|
+
const mockServer = {
|
|
7
|
+
registerTool: vi.fn(),
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const mockHandler = vi.fn().mockResolvedValue({ content: [{ type: 'text', text: 'success' }] });
|
|
11
|
+
|
|
12
|
+
it('should allow read-only tool in read-only mode', async () => {
|
|
13
|
+
process.env.LIGHTDASH_AI_MODE = SafetyMode.READ_ONLY;
|
|
14
|
+
|
|
15
|
+
registerToolSafe(
|
|
16
|
+
mockServer,
|
|
17
|
+
'test_tool',
|
|
18
|
+
{
|
|
19
|
+
description: 'Test description',
|
|
20
|
+
inputSchema: {},
|
|
21
|
+
annotations: READ_ONLY_DEFAULT,
|
|
22
|
+
},
|
|
23
|
+
mockHandler,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
expect(mockServer.registerTool).toHaveBeenCalled();
|
|
27
|
+
const [name, options, handler] = mockServer.registerTool.mock.calls[0];
|
|
28
|
+
|
|
29
|
+
expect(name).toContain('test_tool');
|
|
30
|
+
expect(options.description).toBe('Test description');
|
|
31
|
+
|
|
32
|
+
const result = await handler({});
|
|
33
|
+
expect(result.content[0].text).toBe('success');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should block destructive tool in read-only mode', async () => {
|
|
37
|
+
process.env.LIGHTDASH_AI_MODE = SafetyMode.READ_ONLY;
|
|
38
|
+
|
|
39
|
+
registerToolSafe(
|
|
40
|
+
mockServer,
|
|
41
|
+
'delete_tool',
|
|
42
|
+
{
|
|
43
|
+
description: 'Delete something',
|
|
44
|
+
inputSchema: {},
|
|
45
|
+
annotations: WRITE_DESTRUCTIVE,
|
|
46
|
+
},
|
|
47
|
+
mockHandler,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const [, options, handler] = mockServer.registerTool.mock.calls[1];
|
|
51
|
+
|
|
52
|
+
expect(options.description).toContain('[DISABLED in read-only mode]');
|
|
53
|
+
|
|
54
|
+
const result = await handler({});
|
|
55
|
+
expect(result.isError).toBe(true);
|
|
56
|
+
expect(result.content[0].text).toContain('disabled in read-only mode');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should allow destructive tool in write-destructive mode', async () => {
|
|
60
|
+
process.env.LIGHTDASH_AI_MODE = SafetyMode.WRITE_DESTRUCTIVE;
|
|
61
|
+
|
|
62
|
+
registerToolSafe(
|
|
63
|
+
mockServer,
|
|
64
|
+
'delete_tool_2',
|
|
65
|
+
{
|
|
66
|
+
description: 'Delete something 2',
|
|
67
|
+
inputSchema: {},
|
|
68
|
+
annotations: WRITE_DESTRUCTIVE,
|
|
69
|
+
},
|
|
70
|
+
mockHandler,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const [, options, handler] = mockServer.registerTool.mock.calls[2];
|
|
74
|
+
|
|
75
|
+
expect(options.description).toBe('Delete something 2');
|
|
76
|
+
|
|
77
|
+
const result = await handler({});
|
|
78
|
+
expect(result.content[0].text).toBe('success');
|
|
79
|
+
});
|
|
80
|
+
});
|
package/src/tools/shared.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { LightdashClient } from '@lightdash-tools/client';
|
|
6
|
+
import { isAllowed, getSafetyModeFromEnv, READ_ONLY_DEFAULT } from '@lightdash-tools/common';
|
|
7
|
+
import type { ToolAnnotations } from '@lightdash-tools/common';
|
|
6
8
|
import type { z } from 'zod';
|
|
7
9
|
import { toMcpErrorMessage } from '../errors.js';
|
|
8
10
|
|
|
@@ -11,20 +13,12 @@ export const TOOL_PREFIX = 'lightdash_tools__';
|
|
|
11
13
|
|
|
12
14
|
export type TextContent = {
|
|
13
15
|
content: Array<{ type: 'text'; text: string }>;
|
|
16
|
+
isError?: boolean;
|
|
14
17
|
};
|
|
15
18
|
|
|
16
19
|
/** Tool handler type used to avoid deep instantiation with SDK/Zod. Accepts (args, extra) for SDK compatibility. */
|
|
17
20
|
export type ToolHandler = (args: unknown, extra?: unknown) => Promise<TextContent>;
|
|
18
21
|
|
|
19
|
-
/** MCP tool annotations (hints for client display and approval). See MCP spec Tool annotations. */
|
|
20
|
-
export type ToolAnnotations = {
|
|
21
|
-
title?: string;
|
|
22
|
-
readOnlyHint?: boolean;
|
|
23
|
-
destructiveHint?: boolean;
|
|
24
|
-
idempotentHint?: boolean;
|
|
25
|
-
openWorldHint?: boolean;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
22
|
/** Options for registerTool; inputSchema typed as ZodRawShapeCompat for SDK compatibility. Pass annotations explicitly (e.g. READ_ONLY_DEFAULT or WRITE_IDEMPOTENT) for visibility. */
|
|
29
23
|
export type ToolOptions = {
|
|
30
24
|
description: string;
|
|
@@ -33,29 +27,8 @@ export type ToolOptions = {
|
|
|
33
27
|
annotations?: ToolAnnotations;
|
|
34
28
|
};
|
|
35
29
|
|
|
36
|
-
|
|
37
|
-
export
|
|
38
|
-
readOnlyHint: true,
|
|
39
|
-
openWorldHint: false,
|
|
40
|
-
destructiveHint: false,
|
|
41
|
-
idempotentHint: true,
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/** Preset: write, non-destructive, idempotent (e.g. upsert by slug). Use for create/update tools. */
|
|
45
|
-
export const WRITE_IDEMPOTENT: ToolAnnotations = {
|
|
46
|
-
readOnlyHint: false,
|
|
47
|
-
openWorldHint: false,
|
|
48
|
-
destructiveHint: false,
|
|
49
|
-
idempotentHint: true,
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/** Preset: write, destructive, non-idempotent. Use for delete/remove tools; clients should prompt for user confirmation. */
|
|
53
|
-
export const WRITE_DESTRUCTIVE: ToolAnnotations = {
|
|
54
|
-
readOnlyHint: false,
|
|
55
|
-
openWorldHint: false,
|
|
56
|
-
destructiveHint: true,
|
|
57
|
-
idempotentHint: false,
|
|
58
|
-
};
|
|
30
|
+
// Re-export presets for convenience and backward compatibility in tools
|
|
31
|
+
export { READ_ONLY_DEFAULT, WRITE_IDEMPOTENT, WRITE_DESTRUCTIVE } from '@lightdash-tools/common';
|
|
59
32
|
|
|
60
33
|
/** Internal default for mergeAnnotations; READ_ONLY_DEFAULT is the exported preset. */
|
|
61
34
|
const DEFAULT_ANNOTATIONS: ToolAnnotations = READ_ONLY_DEFAULT;
|
|
@@ -76,12 +49,34 @@ export function registerToolSafe(
|
|
|
76
49
|
): void {
|
|
77
50
|
const name = TOOL_PREFIX + shortName;
|
|
78
51
|
const annotations = mergeAnnotations(options.annotations);
|
|
52
|
+
const mode = getSafetyModeFromEnv();
|
|
53
|
+
|
|
54
|
+
const isToolAllowed = isAllowed(mode, annotations);
|
|
55
|
+
|
|
56
|
+
// If not allowed, wrap handler to return an error and update description
|
|
57
|
+
let finalHandler = handler;
|
|
58
|
+
let finalDescription = options.description;
|
|
59
|
+
|
|
60
|
+
if (!isToolAllowed) {
|
|
61
|
+
finalDescription = `[DISABLED in ${mode} mode] ${options.description}`;
|
|
62
|
+
finalHandler = async () => ({
|
|
63
|
+
content: [
|
|
64
|
+
{
|
|
65
|
+
type: 'text',
|
|
66
|
+
text: `Error: Tool '${name}' is disabled in ${mode} mode. To enable it, change LIGHTDASH_AI_MODE.`,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
isError: true,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
79
73
|
const mergedOptions: ToolOptions = {
|
|
80
74
|
...options,
|
|
75
|
+
description: finalDescription,
|
|
81
76
|
title: options.title ?? options.annotations?.title,
|
|
82
77
|
annotations,
|
|
83
78
|
};
|
|
84
|
-
(server as { registerTool: RegisterToolFn }).registerTool(name, mergedOptions,
|
|
79
|
+
(server as { registerTool: RegisterToolFn }).registerTool(name, mergedOptions, finalHandler);
|
|
85
80
|
}
|
|
86
81
|
|
|
87
82
|
export function wrapTool<T>(
|
|
@@ -95,7 +90,7 @@ export function wrapTool<T>(
|
|
|
95
90
|
return await handler(args as T);
|
|
96
91
|
} catch (err) {
|
|
97
92
|
const text = toMcpErrorMessage(err);
|
|
98
|
-
return { content: [{ type: 'text', text }] };
|
|
93
|
+
return { content: [{ type: 'text', text }], isError: true };
|
|
99
94
|
}
|
|
100
95
|
};
|
|
101
96
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tools: tags (list).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
+
import type { LightdashClient } from '@lightdash-tools/client';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { wrapTool, registerToolSafe, READ_ONLY_DEFAULT } from './shared.js';
|
|
9
|
+
|
|
10
|
+
export function registerTagsTools(server: McpServer, client: LightdashClient): void {
|
|
11
|
+
registerToolSafe(
|
|
12
|
+
server,
|
|
13
|
+
'list_tags',
|
|
14
|
+
{
|
|
15
|
+
title: 'List tags',
|
|
16
|
+
description: 'List all tags in a project',
|
|
17
|
+
inputSchema: { projectUuid: z.string().describe('Project UUID') },
|
|
18
|
+
annotations: READ_ONLY_DEFAULT,
|
|
19
|
+
},
|
|
20
|
+
wrapTool(client, (c) => async ({ projectUuid }: { projectUuid: string }) => {
|
|
21
|
+
const result = await c.v1.tags.listTags(projectUuid);
|
|
22
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
}
|