@lightdash-tools/mcp 0.1.2 → 0.2.4

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
@@ -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
 
@@ -28,6 +28,10 @@ npm install -g @lightdash-tools/mcp
28
28
  - `LIGHTDASH_URL` — Lightdash instance base URL (e.g. `https://app.lightdash.cloud`).
29
29
  - `LIGHTDASH_API_KEY` — Personal access token or API key.
30
30
 
31
+ ### Optional (both modes)
32
+
33
+ - `LIGHTDASH_TOOL_SAFETY_MODE` — Safety mode for dynamic enforcement (`read-only`, `write-idempotent`, `write-destructive`). See [Safety Modes](#safety-modes) for details.
34
+
31
35
  ### Streamable HTTP only
32
36
 
33
37
  - `MCP_HTTP_PORT` — Port for the HTTP server (default: `3100`).
@@ -44,6 +48,12 @@ For use with Claude Desktop or IDEs, use `npx`:
44
48
  npx @lightdash-tools/mcp
45
49
  ```
46
50
 
51
+ To hide destructive tools from the agent:
52
+
53
+ ```bash
54
+ npx @lightdash-tools/mcp --safety-mode write-idempotent
55
+ ```
56
+
47
57
  Or if installed globally:
48
58
 
49
59
  ```bash
@@ -64,7 +74,40 @@ With auth disabled (default), any client can call the endpoint. With `MCP_AUTH_E
64
74
 
65
75
  ## Tools
66
76
 
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`.
77
+ The server registers the following tools (names prefixed with `lightdash_tools__`):
78
+
79
+ - **Projects**: `list_projects`, `get_project`, `validate_project`, `get_validation_results`
80
+ - **Explores**: `list_explores`, `get_explore`, `list_dimensions`, `get_field_lineage`
81
+ - **Charts**: `list_charts`, `list_charts_as_code`, `upsert_chart_as_code`
82
+ - **Dashboards**: `list_dashboards`
83
+ - **Spaces**: `list_spaces`, `get_space`
84
+ - **Users**: `list_organization_members`, `get_member`, `delete_member`
85
+ - **Groups**: `list_groups`, `get_group`
86
+ - **Metrics**: `list_metrics`
87
+ - **Schedulers**: `list_schedulers`
88
+ - **Tags**: `list_tags`
89
+ - **Query**: `compile_query`
90
+ - **Content**: `search_content`
91
+
92
+ ### CLI Options
93
+
94
+ - `--http` — Run as HTTP server instead of Stdio.
95
+ - `--safety-mode <mode>` — Filter registered tools by safety mode (`read-only`, `write-idempotent`, `write-destructive`). Tools not allowed in this mode will not be registered, hiding them from AI agents (Static Filtering).
96
+
97
+ ## Safety Modes
98
+
99
+ The MCP server implements a hierarchical safety model. You can control which tools are available to AI agents using the `LIGHTDASH_TOOL_SAFETY_MODE` environment variable or the `--safety-mode` CLI option.
100
+
101
+ - `read-only`: Only allows non-modifying tools (e.g., `list_*`, `get_*`).
102
+ - `write-idempotent`: Allows read tools and non-destructive writes (e.g., `upsert_chart_as_code`).
103
+ - `write-destructive` (default): Allows all tools, including destructive ones (e.g., `delete_member`).
104
+
105
+ ### Enforcement Layers
106
+
107
+ 1. **Dynamic Enforcement (Visible but Disabled)**: Using `LIGHTDASH_TOOL_SAFETY_MODE` environment variable. Tools are registered and visible to the agent, but return an error if called. This allows agents to understand that a capability exists but is restricted.
108
+ 2. **Static Filtering (Hidden)**: Using the `--safety-mode` CLI option. Tools not allowed in the selected mode are not registered at all. They are completely hidden from the AI agent.
109
+
110
+ When a tool is disabled via dynamic enforcement, the server will return a descriptive error message if an agent attempts to call it.
68
111
 
69
112
  ### Destructive tools
70
113
 
package/dist/bin.d.ts CHANGED
@@ -2,4 +2,4 @@
2
2
  /**
3
3
  * MCP server CLI entrypoint.
4
4
  */
5
- declare const args: string[];
5
+ export {};
package/dist/bin.js CHANGED
@@ -36,10 +36,32 @@ var __importStar = (this && this.__importStar) || (function () {
36
36
  return result;
37
37
  };
38
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
- }
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ const commander_1 = require("commander");
41
+ const common_1 = require("@lightdash-tools/common");
42
+ const config_js_1 = require("./config.js");
43
+ const program = new commander_1.Command();
44
+ program
45
+ .name('lightdash-mcp')
46
+ .description('MCP server for Lightdash AI')
47
+ .version('0.2.3')
48
+ .option('--http', 'Run as HTTP server instead of Stdio')
49
+ .option('--safety-mode <mode>', 'Filter registered tools by safety mode (read-only, write-idempotent, write-destructive)')
50
+ .action((options) => {
51
+ if (options.safetyMode) {
52
+ if (Object.values(common_1.SafetyMode).includes(options.safetyMode)) {
53
+ (0, config_js_1.setStaticSafetyMode)(options.safetyMode);
54
+ }
55
+ else {
56
+ console.error(`Invalid safety mode: ${options.safetyMode}`);
57
+ process.exit(1);
58
+ }
59
+ }
60
+ if (options.http) {
61
+ void Promise.resolve().then(() => __importStar(require('./http.js')));
62
+ }
63
+ else {
64
+ void Promise.resolve().then(() => __importStar(require('./index.js')));
65
+ }
66
+ });
67
+ program.parse(process.argv);
package/dist/config.d.ts CHANGED
@@ -4,6 +4,19 @@
4
4
  */
5
5
  import { LightdashClient } from '@lightdash-tools/client';
6
6
  import type { PartialLightdashClientConfig } from '@lightdash-tools/client';
7
+ import type { SafetyMode } from '@lightdash-tools/common';
8
+ /**
9
+ * Gets the safety mode for dynamic enforcement.
10
+ */
11
+ export declare function getSafetyMode(): SafetyMode;
12
+ /**
13
+ * Gets the safety mode for static tool filtering (binding).
14
+ */
15
+ export declare function getStaticSafetyMode(): SafetyMode | undefined;
16
+ /**
17
+ * Sets the static safety mode (from CLI).
18
+ */
19
+ export declare function setStaticSafetyMode(mode: SafetyMode): void;
7
20
  /**
8
21
  * Builds a LightdashClient from environment variables (and optional overrides).
9
22
  * Throws if LIGHTDASH_URL or LIGHTDASH_API_KEY are missing.
package/dist/config.js CHANGED
@@ -4,8 +4,31 @@
4
4
  * Uses same env vars as CLI: LIGHTDASH_URL, LIGHTDASH_API_KEY.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.getSafetyMode = getSafetyMode;
8
+ exports.getStaticSafetyMode = getStaticSafetyMode;
9
+ exports.setStaticSafetyMode = setStaticSafetyMode;
7
10
  exports.getClient = getClient;
8
11
  const client_1 = require("@lightdash-tools/client");
12
+ const common_1 = require("@lightdash-tools/common");
13
+ let globalStaticSafetyMode;
14
+ /**
15
+ * Gets the safety mode for dynamic enforcement.
16
+ */
17
+ function getSafetyMode() {
18
+ return (0, common_1.getSafetyModeFromEnv)();
19
+ }
20
+ /**
21
+ * Gets the safety mode for static tool filtering (binding).
22
+ */
23
+ function getStaticSafetyMode() {
24
+ return globalStaticSafetyMode;
25
+ }
26
+ /**
27
+ * Sets the static safety mode (from CLI).
28
+ */
29
+ function setStaticSafetyMode(mode) {
30
+ globalStaticSafetyMode = mode;
31
+ }
9
32
  /**
10
33
  * Builds a LightdashClient from environment variables (and optional overrides).
11
34
  * Throws if LIGHTDASH_URL or LIGHTDASH_API_KEY are missing.
@@ -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
+ }
@@ -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
  }
@@ -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
+ }
@@ -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
+ }
@@ -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__";
@@ -14,14 +15,6 @@ export type TextContent = {
14
15
  };
15
16
  /** Tool handler type used to avoid deep instantiation with SDK/Zod. Accepts (args, extra) for SDK compatibility. */
16
17
  export type ToolHandler = (args: unknown, extra?: unknown) => Promise<TextContent>;
17
- /** MCP tool annotations (hints for client display and approval). See MCP spec Tool annotations. */
18
- export type ToolAnnotations = {
19
- title?: string;
20
- readOnlyHint?: boolean;
21
- destructiveHint?: boolean;
22
- idempotentHint?: boolean;
23
- openWorldHint?: boolean;
24
- };
25
18
  /** Options for registerTool; inputSchema typed as ZodRawShapeCompat for SDK compatibility. Pass annotations explicitly (e.g. READ_ONLY_DEFAULT or WRITE_IDEMPOTENT) for visibility. */
26
19
  export type ToolOptions = {
27
20
  description: string;
@@ -29,12 +22,7 @@ export type ToolOptions = {
29
22
  title?: string;
30
23
  annotations?: ToolAnnotations;
31
24
  };
32
- /** Preset: read-only, non-destructive, idempotent, closed-world. Use for list/get/compile tools. */
33
- export declare const READ_ONLY_DEFAULT: ToolAnnotations;
34
- /** Preset: write, non-destructive, idempotent (e.g. upsert by slug). Use for create/update tools. */
35
- export declare const WRITE_IDEMPOTENT: ToolAnnotations;
36
- /** Preset: write, destructive, non-idempotent. Use for delete/remove tools; clients should prompt for user confirmation. */
37
- export declare const WRITE_DESTRUCTIVE: ToolAnnotations;
25
+ export { READ_ONLY_DEFAULT, WRITE_IDEMPOTENT, WRITE_DESTRUCTIVE } from '@lightdash-tools/common';
38
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). */
39
27
  export declare function registerToolSafe(server: unknown, shortName: string, options: ToolOptions, handler: ToolHandler): void;
40
28
  export declare function wrapTool<T>(client: LightdashClient, fn: (client: LightdashClient) => (args: T) => Promise<TextContent>): ToolHandler;
@@ -15,32 +15,18 @@ 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");
20
+ const config_js_1 = require("../config.js");
19
21
  /** Prefix for all MCP tool names (disambiguation when multiple servers are connected). */
20
22
  exports.TOOL_PREFIX = 'lightdash_tools__';
21
- /** Preset: read-only, non-destructive, idempotent, closed-world. Use for list/get/compile tools. */
22
- exports.READ_ONLY_DEFAULT = {
23
- readOnlyHint: true,
24
- openWorldHint: false,
25
- destructiveHint: false,
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
- };
23
+ // Re-export presets for convenience and backward compatibility in tools
24
+ var common_2 = require("@lightdash-tools/common");
25
+ Object.defineProperty(exports, "READ_ONLY_DEFAULT", { enumerable: true, get: function () { return common_2.READ_ONLY_DEFAULT; } });
26
+ Object.defineProperty(exports, "WRITE_IDEMPOTENT", { enumerable: true, get: function () { return common_2.WRITE_IDEMPOTENT; } });
27
+ Object.defineProperty(exports, "WRITE_DESTRUCTIVE", { enumerable: true, get: function () { return common_2.WRITE_DESTRUCTIVE; } });
42
28
  /** Internal default for mergeAnnotations; READ_ONLY_DEFAULT is the exported preset. */
43
- const DEFAULT_ANNOTATIONS = exports.READ_ONLY_DEFAULT;
29
+ const DEFAULT_ANNOTATIONS = common_1.READ_ONLY_DEFAULT;
44
30
  /** Merges per-tool annotations with defaults; per-tool values win. */
45
31
  function mergeAnnotations(overrides) {
46
32
  return Object.assign(Object.assign({}, DEFAULT_ANNOTATIONS), overrides);
@@ -50,8 +36,33 @@ function registerToolSafe(server, shortName, options, handler) {
50
36
  var _a, _b;
51
37
  const name = exports.TOOL_PREFIX + shortName;
52
38
  const annotations = mergeAnnotations(options.annotations);
53
- const mergedOptions = Object.assign(Object.assign({}, options), { title: (_a = options.title) !== null && _a !== void 0 ? _a : (_b = options.annotations) === null || _b === void 0 ? void 0 : _b.title, annotations });
54
- server.registerTool(name, mergedOptions, handler);
39
+ // Static Filtering: Skip registration if not allowed in static safety mode
40
+ const staticMode = (0, config_js_1.getStaticSafetyMode)();
41
+ if (staticMode && !(0, common_1.isAllowed)(staticMode, annotations)) {
42
+ return;
43
+ }
44
+ // Dynamic Enforcement: Wrap handler if not allowed in current safety mode (env)
45
+ const mode = (0, config_js_1.getSafetyMode)();
46
+ const isToolAllowed = (0, common_1.isAllowed)(mode, annotations);
47
+ // If not allowed, wrap handler to return an error and update description
48
+ let finalHandler = handler;
49
+ let finalDescription = options.description;
50
+ if (!isToolAllowed) {
51
+ finalDescription = `[DISABLED in ${mode} mode] ${options.description}`;
52
+ finalHandler = () => __awaiter(this, void 0, void 0, function* () {
53
+ return ({
54
+ content: [
55
+ {
56
+ type: 'text',
57
+ text: `Error: Tool '${name}' is disabled in ${mode} mode. To enable it, change LIGHTDASH_TOOL_SAFETY_MODE.`,
58
+ },
59
+ ],
60
+ isError: true,
61
+ });
62
+ });
63
+ }
64
+ 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 });
65
+ server.registerTool(name, mergedOptions, finalHandler);
55
66
  }
56
67
  function wrapTool(client, fn) {
57
68
  const handler = fn(client);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,96 @@
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
+ const config_js_1 = require("../config.js");
16
+ (0, vitest_1.describe)('registerToolSafe', () => {
17
+ const mockServer = {
18
+ registerTool: vitest_1.vi.fn(),
19
+ };
20
+ const mockHandler = vitest_1.vi.fn().mockResolvedValue({ content: [{ type: 'text', text: 'success' }] });
21
+ (0, vitest_1.it)('should allow read-only tool in read-only mode', () => __awaiter(void 0, void 0, void 0, function* () {
22
+ process.env.LIGHTDASH_TOOL_SAFETY_MODE = common_1.SafetyMode.READ_ONLY;
23
+ (0, shared_1.registerToolSafe)(mockServer, 'test_tool', {
24
+ description: 'Test description',
25
+ inputSchema: {},
26
+ annotations: shared_1.READ_ONLY_DEFAULT,
27
+ }, mockHandler);
28
+ (0, vitest_1.expect)(mockServer.registerTool).toHaveBeenCalled();
29
+ const [name, options, handler] = mockServer.registerTool.mock.calls[0];
30
+ (0, vitest_1.expect)(name).toContain('test_tool');
31
+ (0, vitest_1.expect)(options.description).toBe('Test description');
32
+ const result = yield handler({});
33
+ (0, vitest_1.expect)(result.content[0].text).toBe('success');
34
+ }));
35
+ (0, vitest_1.it)('should block destructive tool in read-only mode', () => __awaiter(void 0, void 0, void 0, function* () {
36
+ process.env.LIGHTDASH_TOOL_SAFETY_MODE = common_1.SafetyMode.READ_ONLY;
37
+ (0, shared_1.registerToolSafe)(mockServer, 'delete_tool', {
38
+ description: 'Delete something',
39
+ inputSchema: {},
40
+ annotations: shared_1.WRITE_DESTRUCTIVE,
41
+ }, mockHandler);
42
+ const [, options, handler] = mockServer.registerTool.mock.calls[1];
43
+ (0, vitest_1.expect)(options.description).toContain('[DISABLED in read-only mode]');
44
+ const result = yield handler({});
45
+ (0, vitest_1.expect)(result.isError).toBe(true);
46
+ (0, vitest_1.expect)(result.content[0].text).toContain('disabled in read-only mode');
47
+ }));
48
+ (0, vitest_1.it)('should allow destructive tool in write-destructive mode', () => __awaiter(void 0, void 0, void 0, function* () {
49
+ process.env.LIGHTDASH_TOOL_SAFETY_MODE = common_1.SafetyMode.WRITE_DESTRUCTIVE;
50
+ (0, shared_1.registerToolSafe)(mockServer, 'delete_tool_2', {
51
+ description: 'Delete something 2',
52
+ inputSchema: {},
53
+ annotations: shared_1.WRITE_DESTRUCTIVE,
54
+ }, mockHandler);
55
+ const [, options, handler] = mockServer.registerTool.mock.calls[2];
56
+ (0, vitest_1.expect)(options.description).toBe('Delete something 2');
57
+ const result = yield handler({});
58
+ (0, vitest_1.expect)(result.content[0].text).toBe('success');
59
+ }));
60
+ (0, vitest_1.describe)('static filtering (safety-mode)', () => {
61
+ (0, vitest_1.it)('should skip registration if tool is more permissive than binded mode', () => {
62
+ // Set binded mode to READ_ONLY
63
+ (0, config_js_1.setStaticSafetyMode)(common_1.SafetyMode.READ_ONLY);
64
+ mockServer.registerTool.mockClear();
65
+ (0, shared_1.registerToolSafe)(mockServer, 'destructive_tool_static', {
66
+ description: 'Destructive',
67
+ inputSchema: {},
68
+ annotations: shared_1.WRITE_DESTRUCTIVE,
69
+ }, mockHandler);
70
+ (0, vitest_1.expect)(mockServer.registerTool).not.toHaveBeenCalled();
71
+ });
72
+ (0, vitest_1.it)('should allow registration if tool matches binded mode', () => {
73
+ (0, config_js_1.setStaticSafetyMode)(common_1.SafetyMode.READ_ONLY);
74
+ mockServer.registerTool.mockClear();
75
+ (0, shared_1.registerToolSafe)(mockServer, 'readonly_tool_static', {
76
+ description: 'Read-only',
77
+ inputSchema: {},
78
+ annotations: shared_1.READ_ONLY_DEFAULT,
79
+ }, mockHandler);
80
+ (0, vitest_1.expect)(mockServer.registerTool).toHaveBeenCalled();
81
+ });
82
+ (0, vitest_1.it)('should allow everything if binded mode is undefined', () => {
83
+ // This is a bit tricky since it's a global. We might need a way to reset it.
84
+ // For now, let's assume we can just pass a permissive mode or it was undefined initially.
85
+ // Since we don't have a reset, let's just test that it works when set to DESTRUCTIVE.
86
+ (0, config_js_1.setStaticSafetyMode)(common_1.SafetyMode.WRITE_DESTRUCTIVE);
87
+ mockServer.registerTool.mockClear();
88
+ (0, shared_1.registerToolSafe)(mockServer, 'any_tool_static', {
89
+ description: 'Any',
90
+ inputSchema: {},
91
+ annotations: shared_1.WRITE_DESTRUCTIVE,
92
+ }, mockHandler);
93
+ (0, vitest_1.expect)(mockServer.registerTool).toHaveBeenCalled();
94
+ });
95
+ });
96
+ });
@@ -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;