├── .dockerignore
├── .env.sample
├── .eslintrc.json
├── .github
└── workflows
│ ├── build.yml
│ └── publish.yml
├── .gitignore
├── .node-version
├── .prettierrc
├── Dockerfile
├── LICENSE
├── README.md
├── eslint.config.js
├── examples
├── list_spaces.ts
└── list_spaces_http.ts
├── package-lock.json
├── package.json
├── smithery.yaml
├── src
├── index.ts
├── mcp.ts
├── schemas.ts
└── server.ts
├── ts-node-loader.js
├── tsconfig.build.json
├── tsconfig.dev.json
└── tsconfig.json
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | dist
4 | .git
5 | .gitignore
6 | .env
7 | *.md
8 | .github
--------------------------------------------------------------------------------
/.env.sample:
--------------------------------------------------------------------------------
1 | # Lightdash API settings for examples
2 | EXAMPLES_CLIENT_LIGHTDASH_API_KEY=your_api_key_here
3 | EXAMPLES_CLIENT_LIGHTDASH_API_URL=https://app.lightdash.cloud
4 | EXAMPLES_CLIENT_LIGHTDASH_PROJECT_UUID=your_project_uuid_here
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "eslint:recommended",
4 | "plugin:@typescript-eslint/recommended",
5 | "prettier"
6 | ],
7 | "parser": "@typescript-eslint/parser",
8 | "plugins": ["@typescript-eslint"],
9 | "root": true
10 | }
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v4
17 |
18 | - name: Set up Node.js
19 | uses: actions/setup-node@v4
20 |
21 | - name: Install dependencies
22 | run: npm ci
23 |
24 | - name: Lint
25 | run: npm run lint
26 |
27 | - name: Build
28 | run: npm run build
29 |
30 | - name: Run tests
31 | run: npm test
32 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish Package
2 |
3 | on:
4 | release:
5 | types: [created]
6 |
7 | jobs:
8 | build-and-publish:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 |
13 | - name: Setup Node.js
14 | uses: actions/setup-node@v4
15 | with:
16 | node-version: '20.x'
17 | cache: 'npm'
18 | registry-url: 'https://registry.npmjs.org'
19 |
20 | - name: Install dependencies
21 | run: npm ci
22 |
23 | - name: Build
24 | run: npm run build
25 |
26 | - name: Publish to npm Registry
27 | run: |
28 | echo "registry=https://registry.npmjs.org/" > .npmrc
29 | npm publish --access public
30 | env:
31 | NODE_AUTH_TOKEN: ${{secrets.NPM_REGISTRY_TOKEN}}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 |
--------------------------------------------------------------------------------
/.node-version:
--------------------------------------------------------------------------------
1 | 20.17.0
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "singleQuote": true,
4 | "tabWidth": 2,
5 | "trailingComma": "es5"
6 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:20-slim AS builder
2 |
3 | WORKDIR /app
4 | COPY package*.json ./
5 | RUN npm install
6 | COPY . .
7 | RUN npm run build
8 |
9 | FROM node:20-slim AS runner
10 |
11 | WORKDIR /app
12 | COPY package*.json ./
13 | RUN npm install --production
14 | COPY --from=builder /app/dist ./dist
15 |
16 | EXPOSE 3000
17 | CMD ["npm", "start"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Ryo Okubo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # lightdash-mcp-server
2 | [](https://smithery.ai/server/@syucream/lightdash-mcp-server)
3 | [](https://badge.fury.io/js/lightdash-mcp-server)
4 |
5 | A [MCP(Model Context Protocol)](https://www.anthropic.com/news/model-context-protocol) server that accesses to [Lightdash](https://www.lightdash.com/).
6 |
7 | This server provides MCP-compatible access to Lightdash's API, allowing AI assistants to interact with your Lightdash data through a standardized interface.
8 |
9 |
10 |
11 |
12 |
13 | ## Features
14 |
15 | Available tools:
16 |
17 | - `list_projects` - List all projects in the Lightdash organization
18 | - `get_project` - Get details of a specific project
19 | - `list_spaces` - List all spaces in a project
20 | - `list_charts` - List all charts in a project
21 | - `list_dashboards` - List all dashboards in a project
22 | - `get_custom_metrics` - Get custom metrics for a project
23 | - `get_catalog` - Get catalog for a project
24 | - `get_metrics_catalog` - Get metrics catalog for a project
25 | - `get_charts_as_code` - Get charts as code for a project
26 | - `get_dashboards_as_code` - Get dashboards as code for a project
27 |
28 | ## Quick Start
29 |
30 | ### Installation
31 |
32 | #### Installing via Smithery
33 |
34 | To install Lightdash MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@syucream/lightdash-mcp-server):
35 |
36 | ```bash
37 | npx -y @smithery/cli install lightdash-mcp-server --client claude
38 | ```
39 |
40 | #### Manual Installation
41 | ```bash
42 | npm install lightdash-mcp-server
43 | ```
44 |
45 | ### Configuration
46 |
47 | - `LIGHTDASH_API_KEY`: Your Lightdash PAT
48 | - `LIGHTDASH_API_URL`: The API base URL
49 |
50 | ### Usage
51 |
52 | The lightdash-mcp-server supports two transport modes: **Stdio** (default) and **HTTP**.
53 |
54 | #### Stdio Transport (Default)
55 |
56 | 1. Start the MCP server:
57 |
58 | ```bash
59 | npx lightdash-mcp-server
60 | ```
61 |
62 | 2. Edit your MCP configuration json:
63 | ```json
64 | ...
65 | "lightdash": {
66 | "command": "npx",
67 | "args": [
68 | "-y",
69 | "lightdash-mcp-server"
70 | ],
71 | "env": {
72 | "LIGHTDASH_API_KEY": "",
73 | "LIGHTDASH_API_URL": "https://"
74 | }
75 | },
76 | ...
77 | ```
78 |
79 | #### HTTP Transport (Streamable HTTP)
80 |
81 | 1. Start the MCP server in HTTP mode:
82 |
83 | ```bash
84 | npx lightdash-mcp-server -port 8080
85 | ```
86 |
87 | This starts the server using StreamableHTTPServerTransport, making it accessible via HTTP at `http://localhost:8080/mcp`.
88 |
89 | 2. Configure your MCP client to connect via HTTP:
90 |
91 | **For Claude Desktop and other MCP clients:**
92 |
93 | Edit your MCP configuration json to use the `url` field instead of `command` and `args`:
94 |
95 | ```json
96 | ...
97 | "lightdash": {
98 | "url": "http://localhost:8080/mcp"
99 | },
100 | ...
101 | ```
102 |
103 | **For programmatic access:**
104 |
105 | Use the streamable HTTP client transport:
106 | ```javascript
107 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
108 | import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
109 |
110 | const client = new Client({
111 | name: 'my-client',
112 | version: '1.0.0'
113 | }, {
114 | capabilities: {}
115 | });
116 |
117 | const transport = new StreamableHTTPClientTransport(
118 | new URL('http://localhost:8080/mcp')
119 | );
120 |
121 | await client.connect(transport);
122 | ```
123 |
124 | **Note:** When using HTTP mode, ensure the environment variables `LIGHTDASH_API_KEY` and `LIGHTDASH_API_URL` are set in the environment where the server is running, as they cannot be passed through MCP client configuration.
125 |
126 | See `examples/list_spaces_http.ts` for a complete example of connecting to the HTTP server programmatically.
127 |
128 | ## Development
129 |
130 | ### Available Scripts
131 |
132 | - `npm run dev` - Start the server in development mode with hot reloading (stdio transport)
133 | - `npm run dev:http` - Start the server in development mode with HTTP transport on port 8080
134 | - `npm run build` - Build the project for production
135 | - `npm run start` - Start the production server
136 | - `npm run lint` - Run linting checks (ESLint and Prettier)
137 | - `npm run fix` - Automatically fix linting issues
138 | - `npm run examples` - Run the example scripts
139 |
140 | ### Contributing
141 |
142 | 1. Fork the repository
143 | 2. Create your feature branch
144 | 3. Run tests and linting: `npm run lint`
145 | 4. Commit your changes
146 | 5. Push to the branch
147 | 6. Create a Pull Request
148 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import eslint from '@eslint/js';
2 | import tseslint from 'typescript-eslint';
3 |
4 | export default tseslint.config(
5 | eslint.configs.recommended,
6 | ...tseslint.configs.recommended,
7 | {
8 | files: ['src/**/*.ts'],
9 | languageOptions: {
10 | parser: tseslint.parser,
11 | parserOptions: {
12 | project: './tsconfig.json',
13 | },
14 | },
15 | }
16 | );
--------------------------------------------------------------------------------
/examples/list_spaces.ts:
--------------------------------------------------------------------------------
1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
3 | import { config } from 'dotenv';
4 | import { fileURLToPath } from 'node:url';
5 | import { dirname, resolve } from 'node:path';
6 |
7 | // Get the current file's directory
8 | const __filename = fileURLToPath(import.meta.url);
9 | const __dirname = dirname(__filename);
10 |
11 | // Load environment variables from .env file
12 | config();
13 |
14 | // Get and validate required environment variables
15 | const apiKey = process.env.EXAMPLES_CLIENT_LIGHTDASH_API_KEY;
16 | const apiUrl =
17 | process.env.EXAMPLES_CLIENT_LIGHTDASH_API_URL ??
18 | 'https://app.lightdash.cloud';
19 |
20 | if (!apiKey) {
21 | throw new Error(
22 | 'EXAMPLES_CLIENT_LIGHTDASH_API_KEY environment variable is required'
23 | );
24 | }
25 |
26 | // After validation, we can safely assert these are strings
27 | const env = {
28 | LIGHTDASH_API_KEY: apiKey,
29 | LIGHTDASH_API_URL: apiUrl,
30 | } as const satisfies Record;
31 |
32 | async function main() {
33 | // Initialize MCP client
34 | const client = new Client(
35 | {
36 | name: 'lightdash-mcp-example-client',
37 | version: '1.0.0',
38 | },
39 | {
40 | capabilities: {},
41 | }
42 | );
43 |
44 | // Create transport to connect to the server
45 | const transport = new StdioClientTransport({
46 | command: process.execPath,
47 | args: [
48 | '--import',
49 | resolve(__dirname, '../ts-node-loader.js'),
50 | resolve(__dirname, '../src/index.ts'),
51 | ],
52 | env,
53 | });
54 |
55 | try {
56 | // Connect to the server
57 | await client.connect(transport);
58 |
59 | // List available tools
60 | await client.listTools();
61 |
62 | // Call list_spaces with a project UUID
63 | const projectUuid = process.env.EXAMPLES_CLIENT_LIGHTDASH_PROJECT_UUID;
64 | if (!projectUuid) {
65 | throw new Error(
66 | 'EXAMPLES_CLIENT_LIGHTDASH_PROJECT_UUID environment variable is required'
67 | );
68 | }
69 |
70 | await client.callTool({
71 | name: 'lightdash_list_spaces',
72 | arguments: {
73 | projectUuid,
74 | },
75 | });
76 | } finally {
77 | // Close the connection
78 | await transport.close();
79 | }
80 | }
81 |
82 | main();
83 |
--------------------------------------------------------------------------------
/examples/list_spaces_http.ts:
--------------------------------------------------------------------------------
1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2 | import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3 | import { config } from 'dotenv';
4 |
5 | // Load environment variables from .env file
6 | config();
7 |
8 | // Get and validate required environment variables
9 | const apiKey = process.env.EXAMPLES_CLIENT_LIGHTDASH_API_KEY;
10 | const apiUrl =
11 | process.env.EXAMPLES_CLIENT_LIGHTDASH_API_URL ??
12 | 'https://app.lightdash.cloud';
13 |
14 | if (!apiKey) {
15 | throw new Error(
16 | 'EXAMPLES_CLIENT_LIGHTDASH_API_KEY environment variable is required'
17 | );
18 | }
19 |
20 | // Set environment variables for the HTTP server
21 | process.env.LIGHTDASH_API_KEY = apiKey;
22 | process.env.LIGHTDASH_API_URL = apiUrl;
23 |
24 | async function main() {
25 | // Initialize MCP client
26 | const client = new Client(
27 | {
28 | name: 'lightdash-mcp-http-example-client',
29 | version: '1.0.0',
30 | },
31 | {
32 | capabilities: {},
33 | }
34 | );
35 |
36 | // Create HTTP transport to connect to the server
37 | const transport = new StreamableHTTPClientTransport(
38 | new URL('http://localhost:8080/mcp')
39 | );
40 |
41 | try {
42 | // Connect to the server
43 | await client.connect(transport);
44 |
45 | // List available tools
46 | await client.listTools();
47 |
48 | // Call list_spaces with a project UUID
49 | const projectUuid = process.env.EXAMPLES_CLIENT_LIGHTDASH_PROJECT_UUID;
50 | if (!projectUuid) {
51 | throw new Error(
52 | 'EXAMPLES_CLIENT_LIGHTDASH_PROJECT_UUID environment variable is required'
53 | );
54 | }
55 |
56 | await client.callTool({
57 | name: 'lightdash_list_spaces',
58 | arguments: {
59 | projectUuid,
60 | },
61 | });
62 | } finally {
63 | // Close the connection
64 | await transport.close();
65 | }
66 | }
67 |
68 | main();
69 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lightdash-mcp-server",
3 | "version": "0.0.12",
4 | "description": "A MCP(Model Context Protocol) server that accesses to lightdash( https://www.lightdash.com/ )",
5 | "main": "dist/index.js",
6 | "type": "module",
7 | "bin": {
8 | "lightdash-mcp-server": "dist/index.js"
9 | },
10 | "files": [
11 | "dist",
12 | "README.md"
13 | ],
14 | "scripts": {
15 | "dev": "node --import ./ts-node-loader.js src/index.ts",
16 | "dev:http": "node --import ./ts-node-loader.js src/index.ts -port 8080",
17 | "build": "tsc -p tsconfig.build.json && shx chmod +x dist/*.js",
18 | "start": "node dist/index.js",
19 | "test": "echo \"No tests yet\"",
20 | "lint": "npm run lint:eslint && npm run lint:prettier",
21 | "lint:eslint": "eslint \"src/**/*.ts\" \"examples/**/*.ts\"",
22 | "lint:prettier": "prettier --check \"src/**/*.ts\" \"examples/**/*.ts\"",
23 | "fix": "npm run fix:eslint && npm run fix:prettier",
24 | "fix:eslint": "eslint \"src/**/*.ts\" \"examples/**/*.ts\" --fix",
25 | "fix:prettier": "prettier --write \"src/**/*.ts\" \"examples/**/*.ts\"",
26 | "examples": "node --import ./ts-node-loader.js examples/list_spaces.ts"
27 | },
28 | "repository": {
29 | "type": "git",
30 | "url": "git+https://github.com/syucream/lightdash-mcp-server.git"
31 | },
32 | "keywords": [
33 | "lightdash",
34 | "mcp",
35 | "modelcontextprotocol"
36 | ],
37 | "author": "syucream",
38 | "license": "MIT",
39 | "bugs": {
40 | "url": "https://github.com/syucream/lightdash-mcp-server/issues"
41 | },
42 | "homepage": "https://github.com/syucream/lightdash-mcp-server#readme",
43 | "dependencies": {
44 | "@modelcontextprotocol/sdk": "^1.11.4",
45 | "@types/express": "^5.0.0",
46 | "@types/node": "^22.10.3",
47 | "dotenv": "^16.4.7",
48 | "express": "^4.21.2",
49 | "lightdash-client-typescript-fetch": "^0.0.4-202503270130",
50 | "typescript": "^5.7.2",
51 | "typescript-eslint": "^8.19.0",
52 | "zod": "^3.24.1",
53 | "zod-to-json-schema": "^3.24.1"
54 | },
55 | "devDependencies": {
56 | "@typescript-eslint/eslint-plugin": "^8.19.0",
57 | "@typescript-eslint/parser": "^8.19.0",
58 | "eslint": "^9.17.0",
59 | "eslint-config-prettier": "^9.1.0",
60 | "prettier": "^3.4.2",
61 | "shx": "^0.3.4",
62 | "ts-node": "^10.9.2"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
2 |
3 | startCommand:
4 | type: stdio
5 | configSchema:
6 | # JSON Schema defining the configuration options for the MCP.
7 | type: object
8 | required:
9 | - lightdashApiKey
10 | properties:
11 | lightdashApiKey:
12 | type: string
13 | description: The API key for accessing the Lightdash API.
14 | lightdashApiUrl:
15 | type: string
16 | default: https://app.lightdash.cloud
17 | description: The base URL for the Lightdash API.
18 | commandFunction:
19 | # A function that produces the CLI command to start the MCP on stdio.
20 | |-
21 | config => ({ command: 'npx', args: ['lightdash-mcp-server'], env: { LIGHTDASH_API_KEY: config.lightdashApiKey, LIGHTDASH_API_URL: config.lightdashApiUrl } })
22 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5 | import crypto from 'crypto';
6 | import { startHttpServer } from './server.js';
7 | import { server } from './mcp.js';
8 |
9 | let httpPort: number | null = null;
10 | const args = process.argv.slice(2);
11 | const portIndex = args.indexOf('-port');
12 |
13 | if (portIndex !== -1 && args.length > portIndex + 1) {
14 | const portValue = args[portIndex + 1];
15 | const parsedPort = parseInt(portValue, 10);
16 | if (isNaN(parsedPort)) {
17 | console.error(
18 | `Invalid port number provided for -port: "${portValue}". Must be a valid number. Exiting.`
19 | );
20 | process.exit(1);
21 | } else {
22 | httpPort = parsedPort;
23 | }
24 | } else if (portIndex !== -1 && args.length <= portIndex + 1) {
25 | console.error(
26 | 'Error: -port option requires a subsequent port number. Exiting.'
27 | );
28 | process.exit(1);
29 | }
30 |
31 | if (httpPort !== null) {
32 | const httpTransport = new StreamableHTTPServerTransport({
33 | sessionIdGenerator: () => crypto.randomUUID(),
34 | enableJsonResponse: true,
35 | });
36 | server.connect(httpTransport);
37 | startHttpServer(httpTransport, httpPort);
38 |
39 | console.log(
40 | `[INFO] MCP Server is listening on http://localhost:${httpPort}/mcp`
41 | );
42 | await new Promise(() => {}); // Keep process alive
43 | } else {
44 | const stdioTransport = new StdioServerTransport();
45 | server.connect(stdioTransport);
46 | }
47 |
--------------------------------------------------------------------------------
/src/mcp.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ListToolsRequestSchema,
3 | CallToolRequestSchema,
4 | type CallToolRequest,
5 | } from '@modelcontextprotocol/sdk/types.js';
6 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7 | import { createLightdashClient } from 'lightdash-client-typescript-fetch';
8 | import { zodToJsonSchema } from 'zod-to-json-schema';
9 | import {
10 | ListProjectsRequestSchema,
11 | GetProjectRequestSchema,
12 | ListSpacesRequestSchema,
13 | ListChartsRequestSchema,
14 | ListDashboardsRequestSchema,
15 | GetCustomMetricsRequestSchema,
16 | GetCatalogRequestSchema,
17 | GetMetricsCatalogRequestSchema,
18 | GetChartsAsCodeRequestSchema,
19 | GetDashboardsAsCodeRequestSchema,
20 | GetMetadataRequestSchema,
21 | GetAnalyticsRequestSchema,
22 | GetUserAttributesRequestSchema,
23 | } from './schemas.js';
24 |
25 | const lightdashClient = createLightdashClient(
26 | process.env.LIGHTDASH_API_URL || 'https://app.lightdash.cloud',
27 | {
28 | headers: {
29 | Authorization: `ApiKey ${process.env.LIGHTDASH_API_KEY}`,
30 | },
31 | }
32 | );
33 |
34 | export const server = new Server(
35 | {
36 | name: 'lightdash-mcp-server',
37 | version: '0.0.1',
38 | },
39 | {
40 | capabilities: {
41 | tools: {},
42 | },
43 | }
44 | );
45 |
46 | server.setRequestHandler(ListToolsRequestSchema, async () => {
47 | return {
48 | tools: [
49 | {
50 | name: 'lightdash_list_projects',
51 | description: 'List all projects in the Lightdash organization',
52 | inputSchema: zodToJsonSchema(ListProjectsRequestSchema),
53 | },
54 | {
55 | name: 'lightdash_get_project',
56 | description: 'Get details of a specific project',
57 | inputSchema: zodToJsonSchema(GetProjectRequestSchema),
58 | },
59 | {
60 | name: 'lightdash_list_spaces',
61 | description: 'List all spaces in a project',
62 | inputSchema: zodToJsonSchema(ListSpacesRequestSchema),
63 | },
64 | {
65 | name: 'lightdash_list_charts',
66 | description: 'List all charts in a project',
67 | inputSchema: zodToJsonSchema(ListChartsRequestSchema),
68 | },
69 | {
70 | name: 'lightdash_list_dashboards',
71 | description: 'List all dashboards in a project',
72 | inputSchema: zodToJsonSchema(ListDashboardsRequestSchema),
73 | },
74 | {
75 | name: 'lightdash_get_custom_metrics',
76 | description: 'Get custom metrics for a project',
77 | inputSchema: zodToJsonSchema(GetCustomMetricsRequestSchema),
78 | },
79 | {
80 | name: 'lightdash_get_catalog',
81 | description: 'Get catalog for a project',
82 | inputSchema: zodToJsonSchema(GetCatalogRequestSchema),
83 | },
84 | {
85 | name: 'lightdash_get_metrics_catalog',
86 | description: 'Get metrics catalog for a project',
87 | inputSchema: zodToJsonSchema(GetMetricsCatalogRequestSchema),
88 | },
89 | {
90 | name: 'lightdash_get_charts_as_code',
91 | description: 'Get charts as code for a project',
92 | inputSchema: zodToJsonSchema(GetChartsAsCodeRequestSchema),
93 | },
94 | {
95 | name: 'lightdash_get_dashboards_as_code',
96 | description: 'Get dashboards as code for a project',
97 | inputSchema: zodToJsonSchema(GetDashboardsAsCodeRequestSchema),
98 | },
99 | {
100 | name: 'lightdash_get_metadata',
101 | description: 'Get metadata for a specific table in the data catalog',
102 | inputSchema: zodToJsonSchema(GetMetadataRequestSchema),
103 | },
104 | {
105 | name: 'lightdash_get_analytics',
106 | description: 'Get analytics for a specific table in the data catalog',
107 | inputSchema: zodToJsonSchema(GetAnalyticsRequestSchema),
108 | },
109 | {
110 | name: 'lightdash_get_user_attributes',
111 | description: 'Get organization user attributes',
112 | inputSchema: zodToJsonSchema(GetUserAttributesRequestSchema),
113 | },
114 | ],
115 | };
116 | });
117 |
118 | server.setRequestHandler(
119 | CallToolRequestSchema,
120 | async (request: CallToolRequest) => {
121 | try {
122 | if (!request.params) {
123 | throw new Error('Params are required');
124 | }
125 |
126 | switch (request.params.name) {
127 | case 'lightdash_list_projects': {
128 | const { data, error } = await lightdashClient.GET(
129 | '/api/v1/org/projects',
130 | {}
131 | );
132 | if (error) {
133 | throw new Error(
134 | `Lightdash API error: ${error.error.name}, ${
135 | error.error.message ?? 'no message'
136 | }`
137 | );
138 | }
139 | return {
140 | content: [
141 | {
142 | type: 'text',
143 | text: JSON.stringify(data.results, null, 2),
144 | },
145 | ],
146 | };
147 | }
148 | case 'lightdash_get_project': {
149 | const args = GetProjectRequestSchema.parse(request.params.arguments);
150 | const { data, error } = await lightdashClient.GET(
151 | '/api/v1/projects/{projectUuid}',
152 | {
153 | params: {
154 | path: {
155 | projectUuid: args.projectUuid,
156 | },
157 | },
158 | }
159 | );
160 | if (error) {
161 | throw new Error(
162 | `Lightdash API error: ${error.error.name}, ${
163 | error.error.message ?? 'no message'
164 | }`
165 | );
166 | }
167 | return {
168 | content: [
169 | {
170 | type: 'text',
171 | text: JSON.stringify(data.results, null, 2),
172 | },
173 | ],
174 | };
175 | }
176 | case 'lightdash_list_spaces': {
177 | const args = ListSpacesRequestSchema.parse(request.params.arguments);
178 | const { data, error } = await lightdashClient.GET(
179 | '/api/v1/projects/{projectUuid}/spaces',
180 | {
181 | params: {
182 | path: {
183 | projectUuid: args.projectUuid,
184 | },
185 | },
186 | }
187 | );
188 | if (error) {
189 | throw new Error(
190 | `Lightdash API error: ${error.error.name}, ${
191 | error.error.message ?? 'no message'
192 | }`
193 | );
194 | }
195 | return {
196 | content: [
197 | {
198 | type: 'text',
199 | text: JSON.stringify(data.results, null, 2),
200 | },
201 | ],
202 | };
203 | }
204 | case 'lightdash_list_charts': {
205 | const args = ListChartsRequestSchema.parse(request.params.arguments);
206 | const { data, error } = await lightdashClient.GET(
207 | '/api/v1/projects/{projectUuid}/charts',
208 | {
209 | params: {
210 | path: {
211 | projectUuid: args.projectUuid,
212 | },
213 | },
214 | }
215 | );
216 | if (error) {
217 | throw new Error(
218 | `Lightdash API error: ${error.error.name}, ${
219 | error.error.message ?? 'no message'
220 | }`
221 | );
222 | }
223 | return {
224 | content: [
225 | {
226 | type: 'text',
227 | text: JSON.stringify(data.results, null, 2),
228 | },
229 | ],
230 | };
231 | }
232 | case 'lightdash_list_dashboards': {
233 | const args = ListDashboardsRequestSchema.parse(
234 | request.params.arguments
235 | );
236 | const { data, error } = await lightdashClient.GET(
237 | '/api/v1/projects/{projectUuid}/dashboards',
238 | {
239 | params: {
240 | path: {
241 | projectUuid: args.projectUuid,
242 | },
243 | },
244 | }
245 | );
246 | if (error) {
247 | throw new Error(
248 | `Lightdash API error: ${error.error.name}, ${
249 | error.error.message ?? 'no message'
250 | }`
251 | );
252 | }
253 | return {
254 | content: [
255 | {
256 | type: 'text',
257 | text: JSON.stringify(data.results, null, 2),
258 | },
259 | ],
260 | };
261 | }
262 | case 'lightdash_get_custom_metrics': {
263 | const args = GetCustomMetricsRequestSchema.parse(
264 | request.params.arguments
265 | );
266 | const { data, error } = await lightdashClient.GET(
267 | '/api/v1/projects/{projectUuid}/custom-metrics',
268 | {
269 | params: {
270 | path: {
271 | projectUuid: args.projectUuid,
272 | },
273 | },
274 | }
275 | );
276 | if (error) {
277 | throw new Error(
278 | `Lightdash API error: ${error.error.name}, ${
279 | error.error.message ?? 'no message'
280 | }`
281 | );
282 | }
283 | return {
284 | content: [
285 | {
286 | type: 'text',
287 | text: JSON.stringify(data.results, null, 2),
288 | },
289 | ],
290 | };
291 | }
292 | case 'lightdash_get_catalog': {
293 | const args = GetCatalogRequestSchema.parse(request.params.arguments);
294 | const { data, error } = await lightdashClient.GET(
295 | '/api/v1/projects/{projectUuid}/dataCatalog',
296 | {
297 | params: {
298 | path: {
299 | projectUuid: args.projectUuid,
300 | },
301 | },
302 | }
303 | );
304 | if (error) {
305 | throw new Error(
306 | `Lightdash API error: ${error.error.name}, ${
307 | error.error.message ?? 'no message'
308 | }`
309 | );
310 | }
311 | return {
312 | content: [
313 | {
314 | type: 'text',
315 | text: JSON.stringify(data.results, null, 2),
316 | },
317 | ],
318 | };
319 | }
320 | case 'lightdash_get_metrics_catalog': {
321 | const args = GetMetricsCatalogRequestSchema.parse(
322 | request.params.arguments
323 | );
324 | const { data, error } = await lightdashClient.GET(
325 | '/api/v1/projects/{projectUuid}/dataCatalog/metrics',
326 | {
327 | params: {
328 | path: {
329 | projectUuid: args.projectUuid,
330 | },
331 | },
332 | }
333 | );
334 | if (error) {
335 | throw new Error(
336 | `Lightdash API error: ${error.error.name}, ${
337 | error.error.message ?? 'no message'
338 | }`
339 | );
340 | }
341 | return {
342 | content: [
343 | {
344 | type: 'text',
345 | text: JSON.stringify(data.results, null, 2),
346 | },
347 | ],
348 | };
349 | }
350 | case 'lightdash_get_charts_as_code': {
351 | const args = GetChartsAsCodeRequestSchema.parse(
352 | request.params.arguments
353 | );
354 | const { data, error } = await lightdashClient.GET(
355 | '/api/v1/projects/{projectUuid}/charts/code',
356 | {
357 | params: {
358 | path: {
359 | projectUuid: args.projectUuid,
360 | },
361 | },
362 | }
363 | );
364 | if (error) {
365 | throw new Error(
366 | `Lightdash API error: ${error.error.name}, ${
367 | error.error.message ?? 'no message'
368 | }`
369 | );
370 | }
371 | return {
372 | content: [
373 | {
374 | type: 'text',
375 | text: JSON.stringify(data.results, null, 2),
376 | },
377 | ],
378 | };
379 | }
380 | case 'lightdash_get_dashboards_as_code': {
381 | const args = GetDashboardsAsCodeRequestSchema.parse(
382 | request.params.arguments
383 | );
384 | const { data, error } = await lightdashClient.GET(
385 | '/api/v1/projects/{projectUuid}/dashboards/code',
386 | {
387 | params: {
388 | path: {
389 | projectUuid: args.projectUuid,
390 | },
391 | },
392 | }
393 | );
394 | if (error) {
395 | throw new Error(
396 | `Lightdash API error: ${error.error.name}, ${
397 | error.error.message ?? 'no message'
398 | }`
399 | );
400 | }
401 | return {
402 | content: [
403 | {
404 | type: 'text',
405 | text: JSON.stringify(data.results, null, 2),
406 | },
407 | ],
408 | };
409 | }
410 | case 'lightdash_get_metadata': {
411 | const args = GetMetadataRequestSchema.parse(request.params.arguments);
412 | const { data, error } = await lightdashClient.GET(
413 | '/api/v1/projects/{projectUuid}/dataCatalog/{table}/metadata',
414 | {
415 | params: {
416 | path: {
417 | projectUuid: args.projectUuid,
418 | table: args.table,
419 | },
420 | },
421 | }
422 | );
423 | if (error) {
424 | throw new Error(
425 | `Lightdash API error: ${error.error.name}, ${
426 | error.error.message ?? 'no message'
427 | }`
428 | );
429 | }
430 | return {
431 | content: [
432 | {
433 | type: 'text',
434 | text: JSON.stringify(data.results, null, 2),
435 | },
436 | ],
437 | };
438 | }
439 | case 'lightdash_get_analytics': {
440 | const args = GetAnalyticsRequestSchema.parse(
441 | request.params.arguments
442 | );
443 | const { data, error } = await lightdashClient.GET(
444 | '/api/v1/projects/{projectUuid}/dataCatalog/{table}/analytics',
445 | {
446 | params: {
447 | path: {
448 | projectUuid: args.projectUuid,
449 | table: args.table,
450 | },
451 | },
452 | }
453 | );
454 | if (error) {
455 | throw new Error(
456 | `Lightdash API error: ${error.error.name}, ${
457 | error.error.message ?? 'no message'
458 | }`
459 | );
460 | }
461 | return {
462 | content: [
463 | {
464 | type: 'text',
465 | text: JSON.stringify(data.results, null, 2),
466 | },
467 | ],
468 | };
469 | }
470 | case 'lightdash_get_user_attributes': {
471 | const { data, error } = await lightdashClient.GET(
472 | '/api/v1/org/attributes',
473 | {}
474 | );
475 | if (error) {
476 | throw new Error(
477 | `Lightdash API error: ${error.error.name}, ${
478 | error.error.message ?? 'no message'
479 | }`
480 | );
481 | }
482 | return {
483 | content: [
484 | {
485 | type: 'text',
486 | text: JSON.stringify(data.results, null, 2),
487 | },
488 | ],
489 | };
490 | }
491 | default:
492 | throw new Error(`Unknown tool: ${request.params.name}`);
493 | }
494 | } catch (error) {
495 | console.error('Error handling request:', error);
496 | const errorMessage =
497 | error instanceof Error ? error.message : 'Unknown error occurred';
498 | throw new Error(errorMessage);
499 | }
500 | }
501 | );
502 |
--------------------------------------------------------------------------------
/src/schemas.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | export const ListProjectsRequestSchema = z.object({});
4 |
5 | export const GetProjectRequestSchema = z.object({
6 | projectUuid: z
7 | .string()
8 | .uuid()
9 | .describe(
10 | 'The UUID of the project. You can obtain it from the project list.'
11 | ),
12 | });
13 |
14 | export const ListSpacesRequestSchema = z.object({
15 | projectUuid: z
16 | .string()
17 | .uuid()
18 | .describe(
19 | 'The UUID of the project. You can obtain it from the project list.'
20 | ),
21 | });
22 |
23 | export const ListChartsRequestSchema = z.object({
24 | projectUuid: z
25 | .string()
26 | .uuid()
27 | .describe(
28 | 'The UUID of the project. You can obtain it from the project list.'
29 | ),
30 | });
31 |
32 | export const ListDashboardsRequestSchema = z.object({
33 | projectUuid: z
34 | .string()
35 | .uuid()
36 | .describe(
37 | 'The UUID of the project. You can obtain it from the project list.'
38 | ),
39 | });
40 |
41 | export const GetCustomMetricsRequestSchema = z.object({
42 | projectUuid: z
43 | .string()
44 | .uuid()
45 | .describe(
46 | 'The UUID of the project. You can obtain it from the project list.'
47 | ),
48 | });
49 |
50 | export const GetCatalogRequestSchema = z.object({
51 | projectUuid: z
52 | .string()
53 | .uuid()
54 | .describe(
55 | 'The UUID of the project. You can obtain it from the project list.'
56 | ),
57 | });
58 |
59 | export const GetMetricsCatalogRequestSchema = z.object({
60 | projectUuid: z
61 | .string()
62 | .uuid()
63 | .describe(
64 | 'The UUID of the project. You can obtain it from the project list.'
65 | ),
66 | });
67 |
68 | export const GetChartsAsCodeRequestSchema = z.object({
69 | projectUuid: z
70 | .string()
71 | .uuid()
72 | .describe(
73 | 'The UUID of the project. You can obtain it from the project list.'
74 | ),
75 | });
76 |
77 | export const GetDashboardsAsCodeRequestSchema = z.object({
78 | projectUuid: z
79 | .string()
80 | .uuid()
81 | .describe(
82 | 'The UUID of the project. You can obtain it from the project list.'
83 | ),
84 | });
85 |
86 | export const GetMetadataRequestSchema = z.object({
87 | projectUuid: z
88 | .string()
89 | .uuid()
90 | .describe(
91 | 'The UUID of the project. You can obtain it from the project list.'
92 | ),
93 | table: z.string().min(1, 'Table name cannot be empty'),
94 | });
95 |
96 | export const GetAnalyticsRequestSchema = z.object({
97 | projectUuid: z
98 | .string()
99 | .uuid()
100 | .describe(
101 | 'The UUID of the project. You can obtain it from the project list.'
102 | ),
103 | table: z.string(),
104 | });
105 |
106 | export const GetUserAttributesRequestSchema = z.object({});
107 |
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
1 | import express, { type Request, type Response } from 'express';
2 | import { type StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3 |
4 | export function startHttpServer(
5 | httpTransport: StreamableHTTPServerTransport,
6 | port: number
7 | ): void {
8 | const app = express();
9 | app.use(express.json());
10 |
11 | app.all('/mcp', async (req: Request, res: Response) => {
12 | try {
13 | await httpTransport.handleRequest(req, res, req.body);
14 | } catch (error: unknown) {
15 | console.error(`Error handling ${req.method} /mcp request:`, error);
16 | if (!res.headersSent) {
17 | const message =
18 | error instanceof Error ? error.message : 'Internal Server Error';
19 | res
20 | .status(500)
21 | .json({ error: 'Internal Server Error', details: message });
22 | }
23 | }
24 | });
25 |
26 | app
27 | .listen(port, () => {})
28 | .on('error', (err: NodeJS.ErrnoException) => {
29 | if (err.code === 'EADDRINUSE') {
30 | console.error(
31 | `Port ${port} is already in use. Please use a different port.`
32 | );
33 | } else {
34 | console.error('Failed to start Express server:', err);
35 | }
36 | process.exit(1);
37 | });
38 | }
39 |
--------------------------------------------------------------------------------
/ts-node-loader.js:
--------------------------------------------------------------------------------
1 | import { register } from 'node:module';
2 | import { pathToFileURL } from 'node:url';
3 |
4 | register('ts-node/esm', pathToFileURL('./'));
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "rootDir": "./src"
6 | },
7 | "include": ["src/**/*"],
8 | "exclude": ["node_modules", "dist", "examples"]
9 | }
--------------------------------------------------------------------------------
/tsconfig.dev.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist"
5 | },
6 | "include": ["src/**/*", "examples/**/*"],
7 | "exclude": ["node_modules", "dist"]
8 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2022",
4 | "module": "NodeNext",
5 | "strict": true,
6 | "esModuleInterop": true,
7 | "skipLibCheck": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "moduleResolution": "NodeNext",
10 | "resolveJsonModule": true,
11 | "types": ["node"],
12 | "lib": ["es2022"],
13 | "declaration": true,
14 | "sourceMap": true
15 | },
16 | "ts-node": {
17 | "esm": true,
18 | "experimentalSpecifiers": true,
19 | "project": "./tsconfig.dev.json"
20 | }
21 | }
--------------------------------------------------------------------------------