├── .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 | [![smithery badge](https://smithery.ai/badge/@syucream/lightdash-mcp-server)](https://smithery.ai/server/@syucream/lightdash-mcp-server) 3 | [![npm version](https://badge.fury.io/js/lightdash-mcp-server.svg)](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 | Lightdash Server MCP server 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 | } --------------------------------------------------------------------------------