├── package.json ├── Dockerfile ├── LICENSE ├── .github └── workflows │ └── docker-image.yml ├── README.md ├── .gitignore └── index.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kuzu-mcp", 3 | "version": "0.0.1", 4 | "description": "Kuzu MCP server implementation", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@modelcontextprotocol/sdk": "^1.7.0", 13 | "kuzu": "0.11.3" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22 2 | 3 | # Copy app 4 | RUN mkdir -p /home/node/app 5 | COPY ./index.js /home/node/app 6 | COPY ./package.json /home/node/app 7 | COPY ./package-lock.json /home/node/app 8 | RUN chown -R node:node /home/node/app 9 | 10 | # Make database directory and set permissions 11 | RUN mkdir -p /database 12 | RUN chown -R node:node /database 13 | 14 | # Switch to node user 15 | USER node 16 | 17 | # Set working directory 18 | WORKDIR /home/node/app 19 | 20 | # Install dependencies, generate grammar, and reduce size of kuzu node module 21 | # Done in one step to reduce image size 22 | RUN npm install &&\ 23 | rm -rf node_modules/kuzu/prebuilt node_modules/kuzu/kuzu-source 24 | 25 | # Set environment variables 26 | ENV NODE_ENV=production 27 | ENV KUZU_DB_DIR=/database 28 | 29 | # Run app 30 | ENTRYPOINT ["node", "index.js"] 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Kùzu Inc. 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 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Build-And-Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | docker: 11 | name: Build and push Docker image 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: Prebuild - get version 17 | shell: bash 18 | run: | 19 | VERSION=$(node -e 'fs=require("fs");console.log(JSON.parse(fs.readFileSync("package.json")).dependencies.kuzu)') 20 | echo "VERSION=$VERSION" >> $GITHUB_ENV 21 | - name: Set up QEMU 22 | uses: docker/setup-qemu-action@v3 23 | - name: Set up Docker Buildx 24 | uses: docker/setup-buildx-action@v3 25 | - name: Login to Docker Hub 26 | uses: docker/login-action@v3 27 | with: 28 | username: ${{ secrets.DOCKERHUB_USERNAME }} 29 | password: ${{ secrets.DOCKERHUB_TOKEN }} 30 | - name: Login to GitHub Container Registry 31 | uses: docker/login-action@v3 32 | with: 33 | registry: ghcr.io 34 | username: ${{ github.actor }} 35 | password: ${{ secrets.GITHUB_TOKEN }} 36 | - name: Build and push 37 | uses: docker/build-push-action@v5 38 | with: 39 | context: . 40 | platforms: linux/amd64,linux/arm64 41 | push: true 42 | tags: kuzudb/mcp-server:latest, kuzudb/mcp-server:${{ env.VERSION }}, ghcr.io/${{ github.repository_owner }}/mcp-server:latest, ghcr.io/${{ github.repository_owner }}/mcp-server:${{ env.VERSION }} 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kuzu-mcp-server 2 | 3 | A Model Context Protocol server that provides access to Kuzu databases. This server enables LLMs to inspect database schemas and execute queries on provided kuzu database. 4 | 5 | ## Components 6 | ### Tools 7 | - getSchema 8 | - Fetch the full schema of the Kuzu database, including all nodes and relationships tables and their properties 9 | - Input: None 10 | 11 | - query 12 | - Run a Cypher query on the Kuzu database 13 | - Input: `cypher` (string): The Cypher query to run 14 | 15 | ### Prompt 16 | - generateKuzuCypher 17 | - Generate a Cypher query for Kuzu 18 | - Argument: `question` (string): The question in natural language to generate the Cypher query for 19 | 20 | ## Usage with Claude Desktop 21 | ### With Docker (Recommended) 22 | - Edit the configuration file `config.json`: 23 | - on macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` 24 | - on Windows: `%APPDATA%\Claude\claude_desktop_config.json` 25 | - Add the following configuration to the `mcpServers` object: 26 | ```json 27 | { 28 | "mcpServers": { 29 | "kuzu": { 30 | "command": "docker", 31 | "args": [ 32 | "run", 33 | "-v", 34 | "{Path to the directory containing Kuzu database file}:/database", 35 | "-e", 36 | "KUZU_DB_FILE={Kuzu database file name}", 37 | "--rm", 38 | "-i", 39 | "kuzudb/mcp-server" 40 | ] 41 | } 42 | } 43 | } 44 | ``` 45 | Change the `{Path to the directory containing Kuzu database file}` to the actual path 46 | - Restart Claude Desktop 47 | 48 | ### With Node.js and npm (for Development) 49 | - Install dependencies: `npm install` 50 | - Edit the configuration file `config.json`: 51 | - on macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` 52 | - on Windows: `%APPDATA%\Claude\claude_desktop_config.json` 53 | - Add the following configuration to the `mcpServers` object: 54 | ```json 55 | { 56 | "mcpServers": { 57 | "kuzu": { 58 | "command": "node", 59 | "args": [ 60 | "{Absolute Path to this repository}/index.js", 61 | "{Absolute Path to the Kuzu database file}", 62 | ] 63 | } 64 | } 65 | } 66 | ``` 67 | Change the `{Absolute Path to this repository}` and `{Absolute Path to the Kuzu database file}` to the actual paths 68 | - Restart Claude Desktop 69 | 70 | ### Read-Only Mode 71 | The server can be run in read-only mode by setting the `KUZU_READ_ONLY` environment variable to `true`. In this mode, running any query that attempts to modify the database will result in an error. This flag can be set in the configuration file as follows: 72 | ```json 73 | { 74 | "mcpServers": { 75 | "kuzu": { 76 | "command": "docker", 77 | "args": [ 78 | "run", 79 | "-v", 80 | "{Path to the directory containing Kuzu database file}:/database", 81 | "-e", 82 | "KUZU_DB_FILE={Kuzu database file name}", 83 | "-e", 84 | "KUZU_READ_ONLY=true", 85 | "--rm", 86 | "-i", 87 | "kuzudb/mcp-server" 88 | ], 89 | } 90 | } 91 | } 92 | ``` 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,node,macos 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,node,macos 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### macOS Patch ### 34 | # iCloud generated files 35 | *.icloud 36 | 37 | ### Node ### 38 | # Logs 39 | logs 40 | *.log 41 | npm-debug.log* 42 | yarn-debug.log* 43 | yarn-error.log* 44 | lerna-debug.log* 45 | .pnpm-debug.log* 46 | 47 | # Diagnostic reports (https://nodejs.org/api/report.html) 48 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 49 | 50 | # Runtime data 51 | pids 52 | *.pid 53 | *.seed 54 | *.pid.lock 55 | 56 | # Directory for instrumented libs generated by jscoverage/JSCover 57 | lib-cov 58 | 59 | # Coverage directory used by tools like istanbul 60 | coverage 61 | *.lcov 62 | 63 | # nyc test coverage 64 | .nyc_output 65 | 66 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 67 | .grunt 68 | 69 | # Bower dependency directory (https://bower.io/) 70 | bower_components 71 | 72 | # node-waf configuration 73 | .lock-wscript 74 | 75 | # Compiled binary addons (https://nodejs.org/api/addons.html) 76 | build/Release 77 | 78 | # Dependency directories 79 | node_modules/ 80 | jspm_packages/ 81 | 82 | # Snowpack dependency directory (https://snowpack.dev/) 83 | web_modules/ 84 | 85 | # TypeScript cache 86 | *.tsbuildinfo 87 | 88 | # Optional npm cache directory 89 | .npm 90 | 91 | # Optional eslint cache 92 | .eslintcache 93 | 94 | # Optional stylelint cache 95 | .stylelintcache 96 | 97 | # Microbundle cache 98 | .rpt2_cache/ 99 | .rts2_cache_cjs/ 100 | .rts2_cache_es/ 101 | .rts2_cache_umd/ 102 | 103 | # Optional REPL history 104 | .node_repl_history 105 | 106 | # Output of 'npm pack' 107 | *.tgz 108 | 109 | # Yarn Integrity file 110 | .yarn-integrity 111 | 112 | # dotenv environment variable files 113 | .env 114 | .env.development.local 115 | .env.test.local 116 | .env.production.local 117 | .env.local 118 | 119 | # parcel-bundler cache (https://parceljs.org/) 120 | .cache 121 | .parcel-cache 122 | 123 | # Next.js build output 124 | .next 125 | out 126 | 127 | # Nuxt.js build / generate output 128 | .nuxt 129 | dist 130 | 131 | # Gatsby files 132 | .cache/ 133 | # Comment in the public line in if your project uses Gatsby and not Next.js 134 | # https://nextjs.org/blog/next-9-1#public-directory-support 135 | # public 136 | 137 | # vuepress build output 138 | .vuepress/dist 139 | 140 | # vuepress v2.x temp and cache directory 141 | .temp 142 | 143 | # Docusaurus cache and generated files 144 | .docusaurus 145 | 146 | # Serverless directories 147 | .serverless/ 148 | 149 | # FuseBox cache 150 | .fusebox/ 151 | 152 | # DynamoDB Local files 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | .tern-port 157 | 158 | # Stores VSCode versions used for testing VSCode extensions 159 | .vscode-test 160 | 161 | # yarn v2 162 | .yarn/cache 163 | .yarn/unplugged 164 | .yarn/build-state.yml 165 | .yarn/install-state.gz 166 | .pnp.* 167 | 168 | ### Node Patch ### 169 | # Serverless Webpack directories 170 | .webpack/ 171 | 172 | # Optional stylelint cache 173 | 174 | # SvelteKit build / generate output 175 | .svelte-kit 176 | 177 | ### VisualStudioCode ### 178 | .vscode/* 179 | !.vscode/settings.json 180 | !.vscode/tasks.json 181 | !.vscode/launch.json 182 | !.vscode/extensions.json 183 | !.vscode/*.code-snippets 184 | 185 | # Local History for Visual Studio Code 186 | .history/ 187 | 188 | # Built Visual Studio Code Extensions 189 | *.vsix 190 | 191 | ### VisualStudioCode Patch ### 192 | # Ignore all local history of files 193 | .history 194 | .ionide 195 | 196 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,node,macos 197 | 198 | database 199 | history.txt -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { Server } = require("@modelcontextprotocol/sdk/server/index.js"); 2 | const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js"); 3 | const { 4 | ListToolsRequestSchema, 5 | CallToolRequestSchema, 6 | ListPromptsRequestSchema, 7 | GetPromptRequestSchema, 8 | } = require("@modelcontextprotocol/sdk/types.js"); 9 | const kuzu = require("kuzu"); 10 | const path = require("path"); 11 | const TABLE_TYPES = { 12 | NODE: "NODE", 13 | REL: "REL", 14 | }; 15 | 16 | const bigIntReplacer = (_, value) => { 17 | if (typeof value === "bigint") { 18 | return value.toString(); 19 | } 20 | return value; 21 | } 22 | 23 | const server = new Server( 24 | { 25 | name: "kuzu", 26 | version: "0.1.0", 27 | }, 28 | { 29 | capabilities: { 30 | resources: {}, 31 | tools: {}, 32 | prompts: {}, 33 | }, 34 | }, 35 | ); 36 | 37 | let dbPath; 38 | 39 | const args = process.argv.slice(2); 40 | if (args.length === 0) { 41 | const envDbDir = process.env.KUZU_DB_DIR; 42 | const envDbFile = process.env.KUZU_DB_FILE; 43 | if (envDbDir && envDbFile) { 44 | dbPath = path.join(envDbDir, envDbFile); 45 | } else { 46 | console.error("Please provide a path to kuzu database as a command line argument or set KUZU_DB_FILE environment variable if you are launching the server in a container."); 47 | process.exit(1); 48 | } 49 | } else { 50 | dbPath = args[0]; 51 | } 52 | 53 | const isReadOnly = process.env.KUZU_READ_ONLY === "true"; 54 | 55 | process.on("SIGINT", () => { 56 | process.exit(0); 57 | }); 58 | 59 | process.on("SIGTERM", () => { 60 | process.exit(0); 61 | }); 62 | 63 | const db = new kuzu.Database(dbPath, 0, true, isReadOnly); 64 | const conn = new kuzu.Connection(db); 65 | 66 | const getPrompt = (question, schema) => { 67 | const prompt = `Task:Generate Kuzu Cypher statement to query a graph database. 68 | Instructions: 69 | Generate the Kuzu dialect of Cypher with the following rules in mind: 70 | 1. It is recommended to always specifying node and relationship labels explicitly in the \`CREATE\` and \`MERGE\` clause. If not specified, Kuzu will try to infer the label by looking at the schema. 71 | 2. \`FINISH\` is recently introduced in GQL and adopted by Neo4j but not yet supported in Kuzu. You can use \`RETURN COUNT(*)\` instead which will only return one record. 72 | 3. \`FOREACH\` is not supported. You can use \`UNWIND\` instead. 73 | 4. Kuzu can scan files not only in the format of CSV, so the \`LOAD CSV FROM\` clause is renamed to \`LOAD FROM\`. 74 | 5. Relationship cannot be omitted. For example \`--\`, \`-- > \` and \`< --\` are not supported. You need to use \` - [] - \`, \` - [] -> \` and \` < -[] -\` instead. 75 | 6. Neo4j adopts trail semantic (no repeated edge) for pattern within a \`MATCH\` clause. While Kuzu adopts walk semantic (allow repeated edge) for pattern within a \`MATCH\` clause. You can use \`is_trail\` or \`is_acyclic\` function to check if a path is a trail or acyclic. 76 | 7. Since Kuzu adopts trail semantic by default, so a variable length relationship needs to have a upper bound to guarantee the query will terminate. If upper bound is not specified, Kuzu will assign a default value of 30. 77 | 8. To run algorithms like (all) shortest path, simply add \`SHORTEST\` or \`ALL SHORTEST\` between the kleene star and lower bound. For example, \`MATCH(n) - [r * SHORTEST 1..10] -> (m)\`. It is recommended to use \`SHORTEST\` if paths are not needed in the use case. 78 | 9. \`REMOVE\` is not supported. Use \`SET n.prop = NULL\` instead. 79 | 10. Properties must be updated in the form of \`n.prop = expression\`. Update all properties with map of \` +=\` operator is not supported. Try to update properties one by one. 80 | 11. \`USE\` graph is not supported. For Kuzu, each graph is a database. 81 | 12. Using \`WHERE\` inside node or relationship pattern is not supported, e.g. \`MATCH(n: Person WHERE a.name = 'Andy') RETURN n\`. You need to write it as \`MATCH(n: Person) WHERE n.name = 'Andy' RETURN n\`. 82 | 13. Filter on node or relationship labels is not supported, e.g. \`MATCH (n) WHERE n:Person RETURN n\`. You need to write it as \`MATCH(n: Person) RETURN n\`, or \`MATCH(n) WHERE label(n) = 'Person' RETURN n\`. 83 | 14. Any \`SHOW XXX\` clauses become a function call in Kuzu. For example, \`SHOW FUNCTIONS\` in Neo4j is equivalent to \`CALL show_functions() RETURN *\` in Kuzu. 84 | 15. Kuzu supports \`EXISTS\` and \`COUNT\` subquery. 85 | 16. \`CALL \` is not supported. 86 | 87 | Use only the provided node types, relationship types and properties in the schema. 88 | Do not use any other node types, relationship types or properties that are not provided explicitly in the schema. 89 | Schema: 90 | ${JSON.stringify(schema, null, 2)} 91 | Note: Do not include any explanations or apologies in your responses. 92 | Do not respond to any questions that might ask anything else than for you to construct a Cypher statement. 93 | Do not include any text except the generated Cypher statement. 94 | 95 | The question is: 96 | ${question} 97 | `; 98 | return prompt; 99 | }; 100 | 101 | const getSchema = async (connection) => { 102 | const result = await connection.query("CALL show_tables() RETURN *;"); 103 | const tables = await result.getAll(); 104 | result.close(); 105 | const nodeTables = []; 106 | const relTables = []; 107 | for (const table of tables) { 108 | const properties = ( 109 | await connection 110 | .query(`CALL TABLE_INFO('${table.name}') RETURN *;`) 111 | .then((res) => res.getAll()) 112 | ).map((property) => ({ 113 | name: property.name, 114 | type: property.type, 115 | isPrimaryKey: property["primary key"], 116 | })); 117 | if (table.type === TABLE_TYPES.NODE) { 118 | delete table["type"]; 119 | delete table["database name"]; 120 | table.properties = properties; 121 | nodeTables.push(table); 122 | } else if (table.type === TABLE_TYPES.REL) { 123 | delete table["type"]; 124 | delete table["database name"]; 125 | properties.forEach((property) => { 126 | delete property.isPrimaryKey; 127 | }); 128 | table.properties = properties; 129 | const connectivity = await connection 130 | .query(`CALL SHOW_CONNECTION('${table.name}') RETURN *;`) 131 | .then((res) => res.getAll()); 132 | table.connectivity = []; 133 | connectivity.forEach(c => { 134 | table.connectivity.push({ 135 | src: c["source table name"], 136 | dst: c["destination table name"], 137 | }); 138 | }); 139 | relTables.push(table); 140 | } 141 | } 142 | nodeTables.sort((a, b) => a.name.localeCompare(b.name)); 143 | relTables.sort((a, b) => a.name.localeCompare(b.name)); 144 | return { nodeTables, relTables }; 145 | }; 146 | 147 | server.setRequestHandler(ListToolsRequestSchema, async () => { 148 | return { 149 | tools: [ 150 | { 151 | name: "query", 152 | description: "Run a Cypher query on the Kuzu database", 153 | inputSchema: { 154 | type: "object", 155 | properties: { 156 | cypher: { 157 | type: "string", 158 | description: "The Cypher query to run", 159 | }, 160 | }, 161 | }, 162 | }, 163 | { 164 | name: "getSchema", 165 | description: "Get the schema of the Kuzu database", 166 | inputSchema: { 167 | type: "object", 168 | properties: {}, 169 | }, 170 | } 171 | ], 172 | }; 173 | }); 174 | 175 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 176 | if (request.params.name === "query") { 177 | const cypher = request.params.arguments.cypher; 178 | try { 179 | const queryResult = await conn.query(cypher); 180 | const rows = await queryResult.getAll(); 181 | queryResult.close(); 182 | return { 183 | content: [{ 184 | type: "text", text: JSON.stringify(rows, bigIntReplacer, 2) 185 | }], 186 | isError: false, 187 | }; 188 | } catch (error) { 189 | throw error; 190 | } 191 | } else if (request.params.name === "getSchema") { 192 | const schema = await getSchema(conn); 193 | return { 194 | content: [{ type: "text", text: JSON.stringify(schema, null, 2) }], 195 | isError: false, 196 | }; 197 | } 198 | throw new Error(`Unknown tool: ${request.params.name}`); 199 | }); 200 | 201 | server.setRequestHandler(ListPromptsRequestSchema, async () => { 202 | return { 203 | prompts: [ 204 | { 205 | name: "generateKuzuCypher", 206 | description: "Generate a Cypher query for Kuzu", 207 | arguments: [ 208 | { 209 | name: "question", 210 | description: "The question in natural language to generate the Cypher query for", 211 | required: true, 212 | }, 213 | ] 214 | } 215 | ], 216 | }; 217 | }); 218 | 219 | server.setRequestHandler(GetPromptRequestSchema, async (request) => { 220 | if (request.params.name === "generateKuzuCypher") { 221 | const question = request.params.arguments.question; 222 | const schema = await getSchema(conn); 223 | return { 224 | messages: [ 225 | { 226 | role: "user", 227 | content: { 228 | type: "text", 229 | text: getPrompt(question, schema), 230 | } 231 | } 232 | ] 233 | } 234 | } 235 | throw new Error(`Unknown prompt: ${request.params.name}`); 236 | }); 237 | 238 | async function main() { 239 | const transport = new StdioServerTransport(); 240 | await server.connect(transport); 241 | } 242 | 243 | main().catch((error) => { 244 | console.error(error); 245 | }); 246 | --------------------------------------------------------------------------------