@lightdash-tools/mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +53 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +16 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +29 -0
- package/dist/http.d.ts +5 -0
- package/dist/http.js +172 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +36 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +9 -0
- package/dist/tools/charts.d.ts +6 -0
- package/dist/tools/charts.js +55 -0
- package/dist/tools/dashboards.d.ts +6 -0
- package/dist/tools/dashboards.js +28 -0
- package/dist/tools/explores.d.ts +6 -0
- package/dist/tools/explores.js +40 -0
- package/dist/tools/groups.d.ts +6 -0
- package/dist/tools/groups.js +41 -0
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.js +24 -0
- package/dist/tools/projects.d.ts +6 -0
- package/dist/tools/projects.js +37 -0
- package/dist/tools/query.d.ts +6 -0
- package/dist/tools/query.js +34 -0
- package/dist/tools/shared.d.ts +39 -0
- package/dist/tools/shared.js +68 -0
- package/dist/tools/spaces.d.ts +6 -0
- package/dist/tools/spaces.js +40 -0
- package/dist/tools/users.d.ts +6 -0
- package/dist/tools/users.js +50 -0
- package/package.json +23 -0
- package/src/config.ts +16 -0
- package/src/errors.ts +27 -0
- package/src/http.ts +184 -0
- package/src/index.test.ts +8 -0
- package/src/index.ts +28 -0
- package/src/tools/charts.ts +85 -0
- package/src/tools/dashboards.ts +25 -0
- package/src/tools/explores.ts +46 -0
- package/src/tools/groups.ts +49 -0
- package/src/tools/index.ts +25 -0
- package/src/tools/projects.ts +39 -0
- package/src/tools/query.ts +47 -0
- package/src/tools/shared.ts +101 -0
- package/src/tools/spaces.ts +46 -0
- package/src/tools/users.ts +64 -0
- package/tsconfig.json +13 -0
package/src/http.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server entrypoint (Streamable HTTP). Use LIGHTDASH_URL, LIGHTDASH_API_KEY.
|
|
3
|
+
* Optional: MCP_AUTH_ENABLED, MCP_API_KEY. Logging: stderr only.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
8
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
10
|
+
import { getClient } from './config.js';
|
|
11
|
+
import { registerTools } from './tools/index.js';
|
|
12
|
+
|
|
13
|
+
const MCP_PATH = '/mcp';
|
|
14
|
+
const PORT = Number(process.env.MCP_HTTP_PORT ?? '3100');
|
|
15
|
+
|
|
16
|
+
const sessionMap = new Map<string, StreamableHTTPServerTransport>();
|
|
17
|
+
const sharedClient = getClient();
|
|
18
|
+
|
|
19
|
+
function isAuthEnabled(): boolean {
|
|
20
|
+
const v = process.env.MCP_AUTH_ENABLED;
|
|
21
|
+
return v === '1' || v === 'true' || v === 'yes';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getExpectedApiKey(): string | undefined {
|
|
25
|
+
return process.env.MCP_API_KEY;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function authMiddleware(req: IncomingMessage, res: ServerResponse): boolean {
|
|
29
|
+
if (!isAuthEnabled()) return true;
|
|
30
|
+
const expected = getExpectedApiKey();
|
|
31
|
+
if (!expected) {
|
|
32
|
+
console.error('MCP_AUTH_ENABLED is set but MCP_API_KEY is missing');
|
|
33
|
+
res
|
|
34
|
+
.writeHead(500, { 'Content-Type': 'application/json' })
|
|
35
|
+
.end(JSON.stringify({ error: 'Server auth misconfiguration' }));
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const bearer = req.headers.authorization;
|
|
39
|
+
const apiKey = req.headers['x-api-key'];
|
|
40
|
+
const token =
|
|
41
|
+
typeof bearer === 'string' && bearer.startsWith('Bearer ') ? bearer.slice(7).trim() : undefined;
|
|
42
|
+
const key = typeof apiKey === 'string' ? apiKey.trim() : undefined;
|
|
43
|
+
const provided = token ?? key;
|
|
44
|
+
if (!provided || provided !== expected) {
|
|
45
|
+
res
|
|
46
|
+
.writeHead(401, { 'Content-Type': 'application/json' })
|
|
47
|
+
.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createSessionTransport(): StreamableHTTPServerTransport {
|
|
54
|
+
const server = new McpServer({ name: 'lightdash-mcp', version: '1.0.0' });
|
|
55
|
+
registerTools(server, sharedClient);
|
|
56
|
+
|
|
57
|
+
const transport = new StreamableHTTPServerTransport({
|
|
58
|
+
sessionIdGenerator: () => randomUUID(),
|
|
59
|
+
onsessioninitialized: (sessionId) => {
|
|
60
|
+
sessionMap.set(sessionId, transport);
|
|
61
|
+
},
|
|
62
|
+
onsessionclosed: (sessionId) => {
|
|
63
|
+
sessionMap.delete(sessionId);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
server.connect(transport).catch((err) => {
|
|
68
|
+
console.error('MCP server connect error:', err);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return transport;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readBody(req: IncomingMessage): Promise<Buffer> {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
const chunks: Buffer[] = [];
|
|
77
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
78
|
+
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
79
|
+
req.on('error', reject);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseJsonBody(buffer: Buffer): unknown {
|
|
84
|
+
const text = buffer.toString('utf-8');
|
|
85
|
+
if (!text.trim()) return undefined;
|
|
86
|
+
return JSON.parse(text) as unknown;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isInitializeMessage(body: unknown): boolean {
|
|
90
|
+
if (body === undefined) return false;
|
|
91
|
+
const msg = Array.isArray(body) ? body[0] : body;
|
|
92
|
+
return (
|
|
93
|
+
typeof msg === 'object' &&
|
|
94
|
+
msg !== null &&
|
|
95
|
+
'method' in msg &&
|
|
96
|
+
(msg as { method?: string }).method === 'initialize'
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
101
|
+
if (!authMiddleware(req, res)) return;
|
|
102
|
+
|
|
103
|
+
const url = req.url ?? '';
|
|
104
|
+
const path = url.split('?')[0];
|
|
105
|
+
if (path !== MCP_PATH) {
|
|
106
|
+
res
|
|
107
|
+
.writeHead(404, { 'Content-Type': 'application/json' })
|
|
108
|
+
.end(JSON.stringify({ error: 'Not Found' }));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
113
|
+
const sid =
|
|
114
|
+
typeof sessionId === 'string' ? sessionId : Array.isArray(sessionId) ? sessionId[0] : undefined;
|
|
115
|
+
|
|
116
|
+
if (req.method === 'POST') {
|
|
117
|
+
const raw = await readBody(req);
|
|
118
|
+
const body = raw.length > 0 ? parseJsonBody(raw) : undefined;
|
|
119
|
+
|
|
120
|
+
if (sid) {
|
|
121
|
+
const transport = sessionMap.get(sid);
|
|
122
|
+
if (!transport) {
|
|
123
|
+
res
|
|
124
|
+
.writeHead(404, { 'Content-Type': 'application/json' })
|
|
125
|
+
.end(JSON.stringify({ error: 'Session not found' }));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
await transport.handleRequest(req, res, body);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (body !== undefined && isInitializeMessage(body)) {
|
|
133
|
+
const transport = createSessionTransport();
|
|
134
|
+
await transport.handleRequest(req, res, body);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
res.writeHead(400, { 'Content-Type': 'application/json' }).end(
|
|
139
|
+
JSON.stringify({
|
|
140
|
+
error: 'Bad Request: Mcp-Session-Id required for non-initialize requests',
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (req.method === 'GET' || req.method === 'DELETE') {
|
|
147
|
+
if (!sid) {
|
|
148
|
+
res
|
|
149
|
+
.writeHead(400, { 'Content-Type': 'application/json' })
|
|
150
|
+
.end(JSON.stringify({ error: 'Bad Request: Mcp-Session-Id required' }));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const transport = sessionMap.get(sid);
|
|
154
|
+
if (!transport) {
|
|
155
|
+
res
|
|
156
|
+
.writeHead(404, { 'Content-Type': 'application/json' })
|
|
157
|
+
.end(JSON.stringify({ error: 'Session not found' }));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
await transport.handleRequest(req, res);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
res.writeHead(405, { Allow: 'GET, POST, DELETE' }).end();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function main(): void {
|
|
168
|
+
const server = createServer((req, res) => {
|
|
169
|
+
handleRequest(req, res).catch((err) => {
|
|
170
|
+
console.error('MCP HTTP handler error:', err);
|
|
171
|
+
if (!res.headersSent) {
|
|
172
|
+
res
|
|
173
|
+
.writeHead(500, { 'Content-Type': 'application/json' })
|
|
174
|
+
.end(JSON.stringify({ error: 'Internal Server Error' }));
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
server.listen(PORT, () => {
|
|
180
|
+
console.error(`Lightdash MCP server listening on http://localhost:${PORT}${MCP_PATH}`);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
main();
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server entrypoint (Stdio). Use LIGHTDASH_URL and LIGHTDASH_API_KEY.
|
|
3
|
+
* Logging: stderr only (stdout is JSON-RPC).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
|
+
import { getClient } from './config';
|
|
9
|
+
import { registerTools } from './tools';
|
|
10
|
+
|
|
11
|
+
async function main(): Promise<void> {
|
|
12
|
+
const client = getClient();
|
|
13
|
+
const server = new McpServer({
|
|
14
|
+
name: 'lightdash-mcp',
|
|
15
|
+
version: '1.0.0',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
registerTools(server, client);
|
|
19
|
+
|
|
20
|
+
const transport = new StdioServerTransport();
|
|
21
|
+
await server.connect(transport);
|
|
22
|
+
console.error('Lightdash MCP server running on stdio');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
main().catch((err) => {
|
|
26
|
+
console.error('Fatal:', err);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tools: charts (list, charts-as-code list and upsert).
|
|
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, WRITE_IDEMPOTENT } from './shared.js';
|
|
9
|
+
|
|
10
|
+
export function registerChartTools(server: McpServer, client: LightdashClient): void {
|
|
11
|
+
registerToolSafe(
|
|
12
|
+
server,
|
|
13
|
+
'list_charts',
|
|
14
|
+
{
|
|
15
|
+
title: 'List charts',
|
|
16
|
+
description: 'List charts 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 charts = await c.v1.charts.listCharts(projectUuid);
|
|
22
|
+
return { content: [{ type: 'text', text: JSON.stringify(charts, null, 2) }] };
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
registerToolSafe(
|
|
26
|
+
server,
|
|
27
|
+
'list_charts_as_code',
|
|
28
|
+
{
|
|
29
|
+
title: 'List charts as code',
|
|
30
|
+
description: 'Get charts in code representation for a project (for charts-as-code workflows)',
|
|
31
|
+
inputSchema: {
|
|
32
|
+
projectUuid: z.string().describe('Project UUID'),
|
|
33
|
+
ids: z.array(z.string()).optional().describe('Optional chart IDs (slugs) to filter'),
|
|
34
|
+
},
|
|
35
|
+
annotations: READ_ONLY_DEFAULT,
|
|
36
|
+
},
|
|
37
|
+
wrapTool(
|
|
38
|
+
client,
|
|
39
|
+
(c) =>
|
|
40
|
+
async ({ projectUuid, ids }: { projectUuid: string; ids?: string[] }) => {
|
|
41
|
+
const result = await c.v1.charts.getChartsAsCode(projectUuid, { ids });
|
|
42
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
43
|
+
},
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
registerToolSafe(
|
|
47
|
+
server,
|
|
48
|
+
'upsert_chart_as_code',
|
|
49
|
+
{
|
|
50
|
+
title: 'Upsert chart as code',
|
|
51
|
+
description: 'Create or update a chart from its code representation (by slug)',
|
|
52
|
+
inputSchema: {
|
|
53
|
+
projectUuid: z.string().describe('Project UUID'),
|
|
54
|
+
slug: z.string().describe('Chart slug'),
|
|
55
|
+
chart: z
|
|
56
|
+
.record(z.string(), z.unknown())
|
|
57
|
+
.describe('Chart-as-code payload (name, metricQuery, chartConfig, etc.)'),
|
|
58
|
+
},
|
|
59
|
+
annotations: WRITE_IDEMPOTENT,
|
|
60
|
+
},
|
|
61
|
+
wrapTool(
|
|
62
|
+
client,
|
|
63
|
+
(c) =>
|
|
64
|
+
async ({
|
|
65
|
+
projectUuid,
|
|
66
|
+
slug,
|
|
67
|
+
chart,
|
|
68
|
+
}: {
|
|
69
|
+
projectUuid: string;
|
|
70
|
+
slug: string;
|
|
71
|
+
chart: Record<string, unknown>;
|
|
72
|
+
}) => {
|
|
73
|
+
type UpsertChartBody = Parameters<
|
|
74
|
+
LightdashClient['v1']['charts']['upsertChartAsCode']
|
|
75
|
+
>[2];
|
|
76
|
+
const result = await c.v1.charts.upsertChartAsCode(
|
|
77
|
+
projectUuid,
|
|
78
|
+
slug,
|
|
79
|
+
chart as UpsertChartBody,
|
|
80
|
+
);
|
|
81
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
82
|
+
},
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tools: dashboards (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 registerDashboardTools(server: McpServer, client: LightdashClient): void {
|
|
11
|
+
registerToolSafe(
|
|
12
|
+
server,
|
|
13
|
+
'list_dashboards',
|
|
14
|
+
{
|
|
15
|
+
title: 'List dashboards',
|
|
16
|
+
description: 'List dashboards 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 dashboards = await c.v1.dashboards.listDashboards(projectUuid);
|
|
22
|
+
return { content: [{ type: 'text', text: JSON.stringify(dashboards, null, 2) }] };
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tools: explores (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 registerExploresTools(server: McpServer, client: LightdashClient): void {
|
|
11
|
+
registerToolSafe(
|
|
12
|
+
server,
|
|
13
|
+
'list_explores',
|
|
14
|
+
{
|
|
15
|
+
title: 'List explores',
|
|
16
|
+
description: 'List all explores 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 explores = await c.v1.explores.listExplores(projectUuid);
|
|
22
|
+
return { content: [{ type: 'text', text: JSON.stringify(explores, null, 2) }] };
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
registerToolSafe(
|
|
26
|
+
server,
|
|
27
|
+
'get_explore',
|
|
28
|
+
{
|
|
29
|
+
title: 'Get explore',
|
|
30
|
+
description: 'Get an explore by project UUID and explore ID',
|
|
31
|
+
inputSchema: {
|
|
32
|
+
projectUuid: z.string().describe('Project UUID'),
|
|
33
|
+
exploreId: z.string().describe('Explore ID'),
|
|
34
|
+
},
|
|
35
|
+
annotations: READ_ONLY_DEFAULT,
|
|
36
|
+
},
|
|
37
|
+
wrapTool(
|
|
38
|
+
client,
|
|
39
|
+
(c) =>
|
|
40
|
+
async ({ projectUuid, exploreId }: { projectUuid: string; exploreId: string }) => {
|
|
41
|
+
const explore = await c.v1.explores.getExplore(projectUuid, exploreId);
|
|
42
|
+
return { content: [{ type: 'text', text: JSON.stringify(explore, null, 2) }] };
|
|
43
|
+
},
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tools: groups (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
|
+
type ListGroupsParams = {
|
|
11
|
+
page?: number;
|
|
12
|
+
pageSize?: number;
|
|
13
|
+
searchQuery?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function registerGroupTools(server: McpServer, client: LightdashClient): void {
|
|
17
|
+
registerToolSafe(
|
|
18
|
+
server,
|
|
19
|
+
'list_groups',
|
|
20
|
+
{
|
|
21
|
+
title: 'List groups',
|
|
22
|
+
description: 'List organization groups (one page)',
|
|
23
|
+
inputSchema: {
|
|
24
|
+
page: z.number().optional().describe('Page number'),
|
|
25
|
+
pageSize: z.number().optional().describe('Page size'),
|
|
26
|
+
searchQuery: z.string().optional().describe('Search query'),
|
|
27
|
+
},
|
|
28
|
+
annotations: READ_ONLY_DEFAULT,
|
|
29
|
+
},
|
|
30
|
+
wrapTool(client, (c) => async (params: ListGroupsParams) => {
|
|
31
|
+
const result = await c.v1.groups.listGroups(params ?? {});
|
|
32
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
registerToolSafe(
|
|
36
|
+
server,
|
|
37
|
+
'get_group',
|
|
38
|
+
{
|
|
39
|
+
title: 'Get group',
|
|
40
|
+
description: 'Get a group by UUID',
|
|
41
|
+
inputSchema: { groupUuid: z.string().describe('Group UUID') },
|
|
42
|
+
annotations: READ_ONLY_DEFAULT,
|
|
43
|
+
},
|
|
44
|
+
wrapTool(client, (c) => async ({ groupUuid }: { groupUuid: string }) => {
|
|
45
|
+
const group = await c.v1.groups.getGroup(groupUuid);
|
|
46
|
+
return { content: [{ type: 'text', text: JSON.stringify(group, null, 2) }] };
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool registration: barrel that delegates to domain modules.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
+
import type { LightdashClient } from '@lightdash-tools/client';
|
|
7
|
+
import { registerProjectTools } from './projects.js';
|
|
8
|
+
import { registerChartTools } from './charts.js';
|
|
9
|
+
import { registerDashboardTools } from './dashboards.js';
|
|
10
|
+
import { registerSpaceTools } from './spaces.js';
|
|
11
|
+
import { registerUserTools } from './users.js';
|
|
12
|
+
import { registerGroupTools } from './groups.js';
|
|
13
|
+
import { registerQueryTools } from './query.js';
|
|
14
|
+
import { registerExploresTools } from './explores.js';
|
|
15
|
+
|
|
16
|
+
export function registerTools(server: McpServer, client: LightdashClient): void {
|
|
17
|
+
registerProjectTools(server, client);
|
|
18
|
+
registerChartTools(server, client);
|
|
19
|
+
registerDashboardTools(server, client);
|
|
20
|
+
registerSpaceTools(server, client);
|
|
21
|
+
registerUserTools(server, client);
|
|
22
|
+
registerGroupTools(server, client);
|
|
23
|
+
registerQueryTools(server, client);
|
|
24
|
+
registerExploresTools(server, client);
|
|
25
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tools: projects (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 registerProjectTools(server: McpServer, client: LightdashClient): void {
|
|
11
|
+
registerToolSafe(
|
|
12
|
+
server,
|
|
13
|
+
'list_projects',
|
|
14
|
+
{
|
|
15
|
+
title: 'List projects',
|
|
16
|
+
description: 'List all projects in the current organization',
|
|
17
|
+
inputSchema: {},
|
|
18
|
+
annotations: READ_ONLY_DEFAULT,
|
|
19
|
+
},
|
|
20
|
+
wrapTool(client, () => async () => {
|
|
21
|
+
const projects = await client.v1.projects.listProjects();
|
|
22
|
+
return { content: [{ type: 'text', text: JSON.stringify(projects, null, 2) }] };
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
registerToolSafe(
|
|
26
|
+
server,
|
|
27
|
+
'get_project',
|
|
28
|
+
{
|
|
29
|
+
title: 'Get project',
|
|
30
|
+
description: 'Get a project by UUID',
|
|
31
|
+
inputSchema: { projectUuid: z.string().describe('Project UUID') },
|
|
32
|
+
annotations: READ_ONLY_DEFAULT,
|
|
33
|
+
},
|
|
34
|
+
wrapTool(client, (c) => async ({ projectUuid }: { projectUuid: string }) => {
|
|
35
|
+
const project = await c.v1.projects.getProject(projectUuid);
|
|
36
|
+
return { content: [{ type: 'text', text: JSON.stringify(project, null, 2) }] };
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tools: query (compile).
|
|
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 registerQueryTools(server: McpServer, client: LightdashClient): void {
|
|
11
|
+
registerToolSafe(
|
|
12
|
+
server,
|
|
13
|
+
'compile_query',
|
|
14
|
+
{
|
|
15
|
+
title: 'Compile query',
|
|
16
|
+
description: 'Compile a metric query for an explore without executing it',
|
|
17
|
+
inputSchema: {
|
|
18
|
+
projectUuid: z.string().describe('Project UUID'),
|
|
19
|
+
exploreId: z.string().describe('Explore ID'),
|
|
20
|
+
metricQuery: z
|
|
21
|
+
.record(z.string(), z.unknown())
|
|
22
|
+
.describe('Metric query object (dimensions, metrics, filters, etc.)'),
|
|
23
|
+
},
|
|
24
|
+
annotations: READ_ONLY_DEFAULT,
|
|
25
|
+
},
|
|
26
|
+
wrapTool(
|
|
27
|
+
client,
|
|
28
|
+
(c) =>
|
|
29
|
+
async ({
|
|
30
|
+
projectUuid,
|
|
31
|
+
exploreId,
|
|
32
|
+
metricQuery,
|
|
33
|
+
}: {
|
|
34
|
+
projectUuid: string;
|
|
35
|
+
exploreId: string;
|
|
36
|
+
metricQuery: Record<string, unknown>;
|
|
37
|
+
}) => {
|
|
38
|
+
const result = await c.v1.query.compileQuery(
|
|
39
|
+
projectUuid,
|
|
40
|
+
exploreId,
|
|
41
|
+
metricQuery as never,
|
|
42
|
+
);
|
|
43
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
44
|
+
},
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types and helpers for MCP tool registration.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { LightdashClient } from '@lightdash-tools/client';
|
|
6
|
+
import type { z } from 'zod';
|
|
7
|
+
import { toMcpErrorMessage } from '../errors.js';
|
|
8
|
+
|
|
9
|
+
/** Prefix for all MCP tool names (disambiguation when multiple servers are connected). */
|
|
10
|
+
export const TOOL_PREFIX = 'lightdash_tools__';
|
|
11
|
+
|
|
12
|
+
export type TextContent = {
|
|
13
|
+
content: Array<{ type: 'text'; text: string }>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Tool handler type used to avoid deep instantiation with SDK/Zod. Accepts (args, extra) for SDK compatibility. */
|
|
17
|
+
export type ToolHandler = (args: unknown, extra?: unknown) => Promise<TextContent>;
|
|
18
|
+
|
|
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
|
+
/** Options for registerTool; inputSchema typed as ZodRawShapeCompat for SDK compatibility. Pass annotations explicitly (e.g. READ_ONLY_DEFAULT or WRITE_IDEMPOTENT) for visibility. */
|
|
29
|
+
export type ToolOptions = {
|
|
30
|
+
description: string;
|
|
31
|
+
inputSchema: Record<string, z.ZodTypeAny>;
|
|
32
|
+
title?: string;
|
|
33
|
+
annotations?: ToolAnnotations;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Preset: read-only, non-destructive, idempotent, closed-world. Use for list/get/compile tools. */
|
|
37
|
+
export const READ_ONLY_DEFAULT: ToolAnnotations = {
|
|
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
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Internal default for mergeAnnotations; READ_ONLY_DEFAULT is the exported preset. */
|
|
61
|
+
const DEFAULT_ANNOTATIONS: ToolAnnotations = READ_ONLY_DEFAULT;
|
|
62
|
+
|
|
63
|
+
type RegisterToolFn = (name: string, options: ToolOptions, handler: ToolHandler) => void;
|
|
64
|
+
|
|
65
|
+
/** Merges per-tool annotations with defaults; per-tool values win. */
|
|
66
|
+
function mergeAnnotations(overrides?: ToolAnnotations): ToolAnnotations {
|
|
67
|
+
return { ...DEFAULT_ANNOTATIONS, ...overrides };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 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). */
|
|
71
|
+
export function registerToolSafe(
|
|
72
|
+
server: unknown,
|
|
73
|
+
shortName: string,
|
|
74
|
+
options: ToolOptions,
|
|
75
|
+
handler: ToolHandler,
|
|
76
|
+
): void {
|
|
77
|
+
const name = TOOL_PREFIX + shortName;
|
|
78
|
+
const annotations = mergeAnnotations(options.annotations);
|
|
79
|
+
const mergedOptions: ToolOptions = {
|
|
80
|
+
...options,
|
|
81
|
+
title: options.title ?? options.annotations?.title,
|
|
82
|
+
annotations,
|
|
83
|
+
};
|
|
84
|
+
(server as { registerTool: RegisterToolFn }).registerTool(name, mergedOptions, handler);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function wrapTool<T>(
|
|
88
|
+
client: LightdashClient,
|
|
89
|
+
fn: (client: LightdashClient) => (args: T) => Promise<TextContent>,
|
|
90
|
+
): ToolHandler {
|
|
91
|
+
const handler = fn(client);
|
|
92
|
+
return async (args: unknown, extra?: unknown) => {
|
|
93
|
+
void extra;
|
|
94
|
+
try {
|
|
95
|
+
return await handler(args as T);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
const text = toMcpErrorMessage(err);
|
|
98
|
+
return { content: [{ type: 'text', text }] };
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tools: spaces (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 registerSpaceTools(server: McpServer, client: LightdashClient): void {
|
|
11
|
+
registerToolSafe(
|
|
12
|
+
server,
|
|
13
|
+
'list_spaces',
|
|
14
|
+
{
|
|
15
|
+
title: 'List spaces',
|
|
16
|
+
description: 'List spaces 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 spaces = await c.v1.spaces.listSpacesInProject(projectUuid);
|
|
22
|
+
return { content: [{ type: 'text', text: JSON.stringify(spaces, null, 2) }] };
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
registerToolSafe(
|
|
26
|
+
server,
|
|
27
|
+
'get_space',
|
|
28
|
+
{
|
|
29
|
+
title: 'Get space',
|
|
30
|
+
description: 'Get a space by project and space UUID',
|
|
31
|
+
inputSchema: {
|
|
32
|
+
projectUuid: z.string().describe('Project UUID'),
|
|
33
|
+
spaceUuid: z.string().describe('Space UUID'),
|
|
34
|
+
},
|
|
35
|
+
annotations: READ_ONLY_DEFAULT,
|
|
36
|
+
},
|
|
37
|
+
wrapTool(
|
|
38
|
+
client,
|
|
39
|
+
(c) =>
|
|
40
|
+
async ({ projectUuid, spaceUuid }: { projectUuid: string; spaceUuid: string }) => {
|
|
41
|
+
const space = await c.v1.spaces.getSpace(projectUuid, spaceUuid);
|
|
42
|
+
return { content: [{ type: 'text', text: JSON.stringify(space, null, 2) }] };
|
|
43
|
+
},
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
}
|