From 1c37e7e2270228c11daa33dc8190bb7bcf6266d1 Mon Sep 17 00:00:00 2001 From: salimlaimeche Date: Thu, 24 Jul 2025 19:43:34 +0200 Subject: [PATCH 1/2] start ai search mcp server development --- azure-ai-search/.env.example | 0 azure-ai-search/.gitignore | 3 + azure-ai-search/.npmignore | 8 + azure-ai-search/CLAUDE.md | 0 azure-ai-search/LICENSE | 21 + azure-ai-search/package.json | 71 ++ azure-ai-search/pnpm-lock.yaml | 1676 ++++++++++++++++++++++++++++++ azure-ai-search/rollup.config.js | 25 + azure-ai-search/server.ts | 31 + azure-ai-search/tsconfig.json | 16 + azure-ai-search/types.ts | 521 ++++++++++ 11 files changed, 2372 insertions(+) create mode 100644 azure-ai-search/.env.example create mode 100644 azure-ai-search/.gitignore create mode 100644 azure-ai-search/.npmignore create mode 100644 azure-ai-search/CLAUDE.md create mode 100644 azure-ai-search/LICENSE create mode 100644 azure-ai-search/package.json create mode 100644 azure-ai-search/pnpm-lock.yaml create mode 100644 azure-ai-search/rollup.config.js create mode 100644 azure-ai-search/server.ts create mode 100644 azure-ai-search/tsconfig.json create mode 100644 azure-ai-search/types.ts diff --git a/azure-ai-search/.env.example b/azure-ai-search/.env.example new file mode 100644 index 0000000..e69de29 diff --git a/azure-ai-search/.gitignore b/azure-ai-search/.gitignore new file mode 100644 index 0000000..b8245aa --- /dev/null +++ b/azure-ai-search/.gitignore @@ -0,0 +1,3 @@ +/dist +/node_modules +.env \ No newline at end of file diff --git a/azure-ai-search/.npmignore b/azure-ai-search/.npmignore new file mode 100644 index 0000000..c81ba65 --- /dev/null +++ b/azure-ai-search/.npmignore @@ -0,0 +1,8 @@ +src/ +tools/ +prompts/ +type.ts +server.ts +tsconfig.json +pnpm-lock.yaml +node_modules/ \ No newline at end of file diff --git a/azure-ai-search/CLAUDE.md b/azure-ai-search/CLAUDE.md new file mode 100644 index 0000000..e69de29 diff --git a/azure-ai-search/LICENSE b/azure-ai-search/LICENSE new file mode 100644 index 0000000..2fc98ed --- /dev/null +++ b/azure-ai-search/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 IgnitionAI + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/azure-ai-search/package.json b/azure-ai-search/package.json new file mode 100644 index 0000000..bf9b05b --- /dev/null +++ b/azure-ai-search/package.json @@ -0,0 +1,71 @@ +{ + "name": "@ignitionai/azure-ai-search-mcp", + "version": "1.0.0", + "description": "Complete Azure AI Search MCP server", + "type": "module", + "bin": { + "azure-ai-search-mcp": "dist/server.js" + }, + "main": "dist/server.js", + "scripts": { + "build": "rollup -c", + "start": "node --loader ts-node/esm server.ts", + "start:prod": "node dist/server.js", + "inspect": "npx @modelcontextprotocol/inspector node dist/server.js", + "prepublishOnly": "pnpm build", + "test": "echo \"No tests yet\" && exit 0" + }, + "keywords": [ + "azure", + "ai", + "search", + "rag", + "mcp", + "model-context-protocol", + "claude", + "ai", + "database", + "nosql", + "crud", + "batch-operations", + "schema-validation", + "typescript" + ], + "author": "Salim Laimeche ", + "license": "MIT", + "homepage": "https://github.com/IgnitionAI/azure-ai-search-mcp#readme", + "repository": { + "type": "git", + "url": "https://github.com/IgnitionAI/azure-ai-search-mcp.git" + }, + "bugs": { + "url": "https://github.com/IgnitionAI/azure-ai-search-mcp/issues" + }, + "engines": { + "node": ">=18.0.0" + }, + "files": [ + "dist", + "README.md", + "CLAUDE.md", + "LICENSE" + ], + "dependencies": { + "@azure/identity": "^4.10.2", + "@modelcontextprotocol/sdk": "^1.12.1", + "dotenv": "^16.5.0", + "zod": "^3.25.42" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^28.0.3", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-typescript": "^12.1.2", + "rollup": "^4.42.0", + "rollup-plugin-preserve-shebang": "^1.0.1", + "ts-node": "^10.9.1", + "tslib": "^2.8.1", + "typescript": "^5.0.0" + }, + "packageManager": "pnpm@10.11.1+sha512.e519b9f7639869dc8d5c3c5dfef73b3f091094b0a006d7317353c72b124e80e1afd429732e28705ad6bfa1ee879c1fce46c128ccebd3192101f43dd67c667912" +} diff --git a/azure-ai-search/pnpm-lock.yaml b/azure-ai-search/pnpm-lock.yaml new file mode 100644 index 0000000..407797d --- /dev/null +++ b/azure-ai-search/pnpm-lock.yaml @@ -0,0 +1,1676 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@azure/identity': + specifier: ^4.10.2 + version: 4.10.2 + '@modelcontextprotocol/sdk': + specifier: ^1.12.1 + version: 1.16.0 + dotenv: + specifier: ^16.5.0 + version: 16.6.1 + zod: + specifier: ^3.25.42 + version: 3.25.76 + devDependencies: + '@rollup/plugin-commonjs': + specifier: ^28.0.3 + version: 28.0.6(rollup@4.45.1) + '@rollup/plugin-json': + specifier: ^6.1.0 + version: 6.1.0(rollup@4.45.1) + '@rollup/plugin-node-resolve': + specifier: ^16.0.1 + version: 16.0.1(rollup@4.45.1) + '@rollup/plugin-typescript': + specifier: ^12.1.2 + version: 12.1.4(rollup@4.45.1)(tslib@2.8.1)(typescript@5.8.3) + rollup: + specifier: ^4.42.0 + version: 4.45.1 + rollup-plugin-preserve-shebang: + specifier: ^1.0.1 + version: 1.0.1 + ts-node: + specifier: ^10.9.1 + version: 10.9.2(@types/node@24.1.0)(typescript@5.8.3) + tslib: + specifier: ^2.8.1 + version: 2.8.1 + typescript: + specifier: ^5.0.0 + version: 5.8.3 + +packages: + + '@azure/abort-controller@2.1.2': + resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} + engines: {node: '>=18.0.0'} + + '@azure/core-auth@1.10.0': + resolution: {integrity: sha512-88Djs5vBvGbHQHf5ZZcaoNHo6Y8BKZkt3cw2iuJIQzLEgH4Ox6Tm4hjFhbqOxyYsgIG/eJbFEHpxRIfEEWv5Ow==} + engines: {node: '>=20.0.0'} + + '@azure/core-client@1.10.0': + resolution: {integrity: sha512-O4aP3CLFNodg8eTHXECaH3B3CjicfzkxVtnrfLkOq0XNP7TIECGfHpK/C6vADZkWP75wzmdBnsIA8ksuJMk18g==} + engines: {node: '>=20.0.0'} + + '@azure/core-rest-pipeline@1.22.0': + resolution: {integrity: sha512-OKHmb3/Kpm06HypvB3g6Q3zJuvyXcpxDpCS1PnU8OV6AJgSFaee/covXBcPbWc6XDDxtEPlbi3EMQ6nUiPaQtw==} + engines: {node: '>=20.0.0'} + + '@azure/core-tracing@1.3.0': + resolution: {integrity: sha512-+XvmZLLWPe67WXNZo9Oc9CrPj/Tm8QnHR92fFAFdnbzwNdCH1h+7UdpaQgRSBsMY+oW1kHXNUZQLdZ1gHX3ROw==} + engines: {node: '>=20.0.0'} + + '@azure/core-util@1.13.0': + resolution: {integrity: sha512-o0psW8QWQ58fq3i24Q1K2XfS/jYTxr7O1HRcyUE9bV9NttLU+kYOH82Ixj8DGlMTOWgxm1Sss2QAfKK5UkSPxw==} + engines: {node: '>=20.0.0'} + + '@azure/identity@4.10.2': + resolution: {integrity: sha512-Uth4vz0j+fkXCkbvutChUj03PDCokjbC6Wk9JT8hHEUtpy/EurNKAseb3+gO6Zi9VYBvwt61pgbzn1ovk942Qg==} + engines: {node: '>=20.0.0'} + + '@azure/logger@1.3.0': + resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==} + engines: {node: '>=20.0.0'} + + '@azure/msal-browser@4.16.0': + resolution: {integrity: sha512-yF8gqyq7tVnYftnrWaNaxWpqhGQXoXpDfwBtL7UCGlIbDMQ1PUJF/T2xCL6NyDNHoO70qp1xU8GjjYTyNIefkw==} + engines: {node: '>=0.8.0'} + + '@azure/msal-common@15.9.0': + resolution: {integrity: sha512-lbz/D+C9ixUG3hiZzBLjU79a0+5ZXCorjel3mwXluisKNH0/rOS/ajm8yi4yI9RP5Uc70CAcs9Ipd0051Oh/kA==} + engines: {node: '>=0.8.0'} + + '@azure/msal-node@3.6.4': + resolution: {integrity: sha512-jMeut9UQugcmq7aPWWlJKhJIse4DQ594zc/JaP6BIxg55XaX3aM/jcPuIQ4ryHnI4QSf03wUspy/uqAvjWKbOg==} + engines: {node: '>=16'} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.4': + resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@modelcontextprotocol/sdk@1.16.0': + resolution: {integrity: sha512-8ofX7gkZcLj9H9rSd50mCgm3SSF8C7XoclxJuLoV0Cz3rEQ1tv9MZRYYvJtm9n1BiEQQMzSmE/w2AEkNacLYfg==} + engines: {node: '>=18'} + + '@rollup/plugin-commonjs@28.0.6': + resolution: {integrity: sha512-XSQB1K7FUU5QP+3lOQmVCE3I0FcbbNvmNT4VJSj93iUjayaARrTQeoRdiYQoftAJBLrR9t2agwAd3ekaTgHNlw==} + engines: {node: '>=16.0.0 || 14 >= 14.17'} + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-json@6.1.0': + resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-node-resolve@16.0.1': + resolution: {integrity: sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-typescript@12.1.4': + resolution: {integrity: sha512-s5Hx+EtN60LMlDBvl5f04bEiFZmAepk27Q+mr85L/00zPDn1jtzlTV6FWn81MaIwqfWzKxmOJrBWHU6vtQyedQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.14.0||^3.0.0||^4.0.0 + tslib: '*' + typescript: '>=3.7.0' + peerDependenciesMeta: + rollup: + optional: true + tslib: + optional: true + + '@rollup/pluginutils@5.2.0': + resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.45.1': + resolution: {integrity: sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.45.1': + resolution: {integrity: sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.45.1': + resolution: {integrity: sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.45.1': + resolution: {integrity: sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.45.1': + resolution: {integrity: sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.45.1': + resolution: {integrity: sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.45.1': + resolution: {integrity: sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.45.1': + resolution: {integrity: sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.45.1': + resolution: {integrity: sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.45.1': + resolution: {integrity: sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.45.1': + resolution: {integrity: sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.45.1': + resolution: {integrity: sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.45.1': + resolution: {integrity: sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.45.1': + resolution: {integrity: sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.45.1': + resolution: {integrity: sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.45.1': + resolution: {integrity: sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.45.1': + resolution: {integrity: sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.45.1': + resolution: {integrity: sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.45.1': + resolution: {integrity: sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.45.1': + resolution: {integrity: sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==} + cpu: [x64] + os: [win32] + + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@24.1.0': + resolution: {integrity: sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==} + + '@types/resolve@1.20.2': + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + + '@typespec/ts-http-runtime@0.3.0': + resolution: {integrity: sha512-sOx1PKSuFwnIl7z4RN0Ls7N9AQawmR9r66eI5rFCzLDIs8HTIYrIpH9QjYWoX0lkgGrkLxXhi4QnK7MizPRrIg==} + engines: {node: '>=20.0.0'} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + default-browser-id@5.0.0: + resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==} + engines: {node: '>=18'} + + default-browser@5.2.1: + resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.0.3: + resolution: {integrity: sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==} + engines: {node: '>=20.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + + jwa@1.4.2: + resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + magic-string@0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-to-regexp@8.2.0: + resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} + engines: {node: '>=16'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pkce-challenge@5.0.0: + resolution: {integrity: sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==} + engines: {node: '>=16.20.0'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + rollup-plugin-preserve-shebang@1.0.1: + resolution: {integrity: sha512-gk7ExGBqvUinhgrvldKHkAKXXwRkWMXMZymNkrtn50uBgHITlhRjhnKmbNGwAIc4Bzgl3yLv7/8Fhi/XeHhFKg==} + + rollup@4.45.1: + resolution: {integrity: sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + run-applescript@7.0.0: + resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} + engines: {node: '>=18'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.8.0: + resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + zod-to-json-schema@3.24.6: + resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} + peerDependencies: + zod: ^3.24.1 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@azure/abort-controller@2.1.2': + dependencies: + tslib: 2.8.1 + + '@azure/core-auth@1.10.0': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-util': 1.13.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-client@1.10.0': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.0 + '@azure/core-rest-pipeline': 1.22.0 + '@azure/core-tracing': 1.3.0 + '@azure/core-util': 1.13.0 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-rest-pipeline@1.22.0': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.0 + '@azure/core-tracing': 1.3.0 + '@azure/core-util': 1.13.0 + '@azure/logger': 1.3.0 + '@typespec/ts-http-runtime': 0.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-tracing@1.3.0': + dependencies: + tslib: 2.8.1 + + '@azure/core-util@1.13.0': + dependencies: + '@azure/abort-controller': 2.1.2 + '@typespec/ts-http-runtime': 0.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/identity@4.10.2': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.0 + '@azure/core-client': 1.10.0 + '@azure/core-rest-pipeline': 1.22.0 + '@azure/core-tracing': 1.3.0 + '@azure/core-util': 1.13.0 + '@azure/logger': 1.3.0 + '@azure/msal-browser': 4.16.0 + '@azure/msal-node': 3.6.4 + open: 10.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/logger@1.3.0': + dependencies: + '@typespec/ts-http-runtime': 0.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/msal-browser@4.16.0': + dependencies: + '@azure/msal-common': 15.9.0 + + '@azure/msal-common@15.9.0': {} + + '@azure/msal-node@3.6.4': + dependencies: + '@azure/msal-common': 15.9.0 + jsonwebtoken: 9.0.2 + uuid: 8.3.2 + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.4': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.4 + + '@modelcontextprotocol/sdk@1.16.0': + dependencies: + ajv: 6.12.6 + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.3 + express: 5.1.0 + express-rate-limit: 7.5.1(express@5.1.0) + pkce-challenge: 5.0.0 + raw-body: 3.0.0 + zod: 3.25.76 + zod-to-json-schema: 3.24.6(zod@3.25.76) + transitivePeerDependencies: + - supports-color + + '@rollup/plugin-commonjs@28.0.6(rollup@4.45.1)': + dependencies: + '@rollup/pluginutils': 5.2.0(rollup@4.45.1) + commondir: 1.0.1 + estree-walker: 2.0.2 + fdir: 6.4.6(picomatch@4.0.3) + is-reference: 1.2.1 + magic-string: 0.30.17 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.45.1 + + '@rollup/plugin-json@6.1.0(rollup@4.45.1)': + dependencies: + '@rollup/pluginutils': 5.2.0(rollup@4.45.1) + optionalDependencies: + rollup: 4.45.1 + + '@rollup/plugin-node-resolve@16.0.1(rollup@4.45.1)': + dependencies: + '@rollup/pluginutils': 5.2.0(rollup@4.45.1) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.10 + optionalDependencies: + rollup: 4.45.1 + + '@rollup/plugin-typescript@12.1.4(rollup@4.45.1)(tslib@2.8.1)(typescript@5.8.3)': + dependencies: + '@rollup/pluginutils': 5.2.0(rollup@4.45.1) + resolve: 1.22.10 + typescript: 5.8.3 + optionalDependencies: + rollup: 4.45.1 + tslib: 2.8.1 + + '@rollup/pluginutils@5.2.0(rollup@4.45.1)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.45.1 + + '@rollup/rollup-android-arm-eabi@4.45.1': + optional: true + + '@rollup/rollup-android-arm64@4.45.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.45.1': + optional: true + + '@rollup/rollup-darwin-x64@4.45.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.45.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.45.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.45.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.45.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.45.1': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.45.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.45.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.45.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.45.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.45.1': + optional: true + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/estree@1.0.8': {} + + '@types/node@24.1.0': + dependencies: + undici-types: 7.8.0 + + '@types/resolve@1.20.2': {} + + '@typespec/ts-http-runtime@0.3.0': + dependencies: + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + agent-base@7.1.4: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + arg@4.1.3: {} + + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.1 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.0 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + buffer-equal-constant-time@1.0.1: {} + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.0.0 + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + commondir@1.0.1: {} + + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + create-require@1.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + deepmerge@4.3.1: {} + + default-browser-id@5.0.0: {} + + default-browser@5.2.1: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.0 + + define-lazy-prop@3.0.0: {} + + depd@2.0.0: {} + + diff@4.0.2: {} + + dotenv@16.6.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + encodeurl@2.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + escape-html@1.0.3: {} + + estree-walker@2.0.2: {} + + etag@1.8.1: {} + + eventsource-parser@3.0.3: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.3 + + express-rate-limit@7.5.1(express@5.1.0): + dependencies: + express: 5.1.0 + + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fdir@6.4.6(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + finalhandler@2.1.0: + dependencies: + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-docker@3.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-module@1.0.0: {} + + is-promise@4.0.0: {} + + is-reference@1.2.1: + dependencies: + '@types/estree': 1.0.8 + + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + + isexe@2.0.0: {} + + json-schema-traverse@0.4.1: {} + + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.2 + + jwa@1.4.2: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.2 + safe-buffer: 5.2.1 + + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.once@4.1.1: {} + + magic-string@0.25.9: + dependencies: + sourcemap-codec: 1.4.8 + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + + make-error@1.3.6: {} + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.54.0: {} + + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + + ms@2.1.3: {} + + negotiator@1.0.0: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + open@10.2.0: + dependencies: + default-browser: 5.2.1 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + + parseurl@1.3.3: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-to-regexp@8.2.0: {} + + picomatch@4.0.3: {} + + pkce-challenge@5.0.0: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + punycode@2.3.1: {} + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@3.0.0: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + unpipe: 1.0.0 + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + rollup-plugin-preserve-shebang@1.0.1: + dependencies: + magic-string: 0.25.9 + + rollup@4.45.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.45.1 + '@rollup/rollup-android-arm64': 4.45.1 + '@rollup/rollup-darwin-arm64': 4.45.1 + '@rollup/rollup-darwin-x64': 4.45.1 + '@rollup/rollup-freebsd-arm64': 4.45.1 + '@rollup/rollup-freebsd-x64': 4.45.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.45.1 + '@rollup/rollup-linux-arm-musleabihf': 4.45.1 + '@rollup/rollup-linux-arm64-gnu': 4.45.1 + '@rollup/rollup-linux-arm64-musl': 4.45.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.45.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.45.1 + '@rollup/rollup-linux-riscv64-gnu': 4.45.1 + '@rollup/rollup-linux-riscv64-musl': 4.45.1 + '@rollup/rollup-linux-s390x-gnu': 4.45.1 + '@rollup/rollup-linux-x64-gnu': 4.45.1 + '@rollup/rollup-linux-x64-musl': 4.45.1 + '@rollup/rollup-win32-arm64-msvc': 4.45.1 + '@rollup/rollup-win32-ia32-msvc': 4.45.1 + '@rollup/rollup-win32-x64-msvc': 4.45.1 + fsevents: 2.3.3 + + router@2.2.0: + dependencies: + debug: 4.4.1 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.2.0 + transitivePeerDependencies: + - supports-color + + run-applescript@7.0.0: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + semver@7.7.2: {} + + send@1.2.0: + dependencies: + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + sourcemap-codec@1.4.8: {} + + statuses@2.0.1: {} + + statuses@2.0.2: {} + + supports-preserve-symlinks-flag@1.0.0: {} + + toidentifier@1.0.1: {} + + ts-node@10.9.2(@types/node@24.1.0)(typescript@5.8.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 24.1.0 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.8.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + tslib@2.8.1: {} + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + + typescript@5.8.3: {} + + undici-types@7.8.0: {} + + unpipe@1.0.0: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + uuid@8.3.2: {} + + v8-compile-cache-lib@3.0.1: {} + + vary@1.1.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrappy@1.0.2: {} + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.0 + + yn@3.1.1: {} + + zod-to-json-schema@3.24.6(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod@3.25.76: {} diff --git a/azure-ai-search/rollup.config.js b/azure-ai-search/rollup.config.js new file mode 100644 index 0000000..a7f1ccf --- /dev/null +++ b/azure-ai-search/rollup.config.js @@ -0,0 +1,25 @@ +import resolve from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import typescript from '@rollup/plugin-typescript'; +import json from '@rollup/plugin-json'; +import shebang from 'rollup-plugin-preserve-shebang'; + +export default { + input: 'server.ts', + output: { + file: 'dist/server.js', + format: 'esm' + }, + plugins: [ + shebang(), // <- pour le #!/usr/bin/env node + resolve(), + commonjs(), + json(), + typescript({ tsconfig: './tsconfig.json' }) + ], + external: [ + "@modelcontextprotocol/sdk", + "zod", + "dotenv", + ] +}; diff --git a/azure-ai-search/server.ts b/azure-ai-search/server.ts new file mode 100644 index 0000000..931f3fc --- /dev/null +++ b/azure-ai-search/server.ts @@ -0,0 +1,31 @@ +#!/usr/bin/env node +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; +import dotenv from "dotenv"; + +dotenv.config(); + +// Create server instance +const server = new McpServer({ + name: "AzureAISearchMCP", + version: "1.0.0", + description: "MCP server for interacting with Azure AI Search" +}); + + +// Start the server +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("Azure Storage MCP Server running on stdio"); +} + + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("Azure AI Searh MCP Server running on stdio"); + +main().catch((error) => { + console.error("Fatal error in main():", error); + process.exit(1); +}); diff --git a/azure-ai-search/tsconfig.json b/azure-ai-search/tsconfig.json new file mode 100644 index 0000000..c026f96 --- /dev/null +++ b/azure-ai-search/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "module": "ESNext", + "target": "ES2020", + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "declaration": true, + "skipLibCheck": true, + "sourceMap": false, + "removeComments": false + }, + "include": ["server.ts", "tools/**/*.ts", "resources/**/*.ts", "prompts/**/*.ts", "types.ts"] + } + \ No newline at end of file diff --git a/azure-ai-search/types.ts b/azure-ai-search/types.ts new file mode 100644 index 0000000..ed30420 --- /dev/null +++ b/azure-ai-search/types.ts @@ -0,0 +1,521 @@ +import { z } from "zod"; + +// Validation pour les noms de table Azure +const azureTableNameRegex = /^[a-zA-Z][a-zA-Z0-9]{2,62}$/; +const azureKeyRegex = /^[^/\\#?]*$/; + +// Schema pour les paramètres de connexion Azure +export const AzureTableConfigSchema = z.object({ + accountName: z.string(), + accountKey: z.string().optional(), + tableName: z.string(), + connectionString: z.string().optional(), +}); + +export type AzureTableConfig = z.infer; + +// Schema pour les filtres de requête +export const TableQuerySchema = z.object({ + filter: z.string().optional(), + select: z.array(z.string()).optional(), + maxResults: z.number().optional(), +}); + +export type TableQuery = z.infer; + +// Schema pour les entités de table Azure +export const TableEntitySchema = z.record(z.any()); + +export type TableEntity = z.infer; + +// Schema pour les paramètres d'outil MCP +export const ReadTableToolSchema = z.object({ + tableName: z.string().describe("Nom de la table Azure à lire"), + filter: z.string().optional().describe("Filtre OData pour les entités (ex: 'PartitionKey eq \"partition1\"')"), + select: z.array(z.string()).optional().describe("Colonnes à sélectionner"), + maxResults: z.number().optional().describe("Nombre maximum d'entités à retourner"), +}); + +// Schema pour les requêtes avancées avec pagination +export const QueryTableAdvancedSchema = z.object({ + tableName: z.string() + .regex(azureTableNameRegex, "Le nom de table doit commencer par une lettre et contenir seulement des lettres/chiffres (3-63 caractères)") + .describe("Nom de la table Azure à interroger"), + filter: z.string().optional().describe("Filtre OData complexe (ex: 'Age gt 18 and Status eq \"active\"')"), + select: z.array(z.string()).optional().describe("Colonnes à sélectionner"), + orderBy: z.array(z.string()).optional().describe("Tri par colonnes (ex: ['Age desc', 'Name asc'])"), + top: z.number().min(1).max(1000).optional().describe("Nombre d'entités à retourner (1-1000)"), + skip: z.number().min(0).optional().describe("Nombre d'entités à ignorer"), + continuationToken: z.string().optional().describe("Token de continuation pour la pagination"), +}); + +export type QueryTableAdvancedParams = z.infer; + +export type ReadTableToolParams = z.infer; + +// Schema pour les valeurs autorisées dans Azure Table +const AzureTableValueSchema = z.union([ + z.string().max(64000), // String max 64KB + z.number().int().min(-2147483648).max(2147483647), // Int32 + z.number().min(-1.79E+308).max(1.79E+308), // Double + z.boolean(), + z.date(), + z.instanceof(Uint8Array).refine(data => data.length <= 64000, "Binary data must be <= 64KB"), + z.null() +]); + +// Schema pour créer une entité +export const CreateEntityToolSchema = z.object({ + tableName: z.string() + .regex(azureTableNameRegex, "Le nom de table doit commencer par une lettre et contenir seulement des lettres/chiffres (3-63 caractères)") + .describe("Nom de la table Azure"), + partitionKey: z.string() + .min(1, "PartitionKey ne peut pas être vide") + .max(1024, "PartitionKey ne peut pas dépasser 1024 caractères") + .regex(azureKeyRegex, "PartitionKey ne peut pas contenir les caractères /, \\, #, ?") + .describe("Clé de partition de l'entité"), + rowKey: z.string() + .min(1, "RowKey ne peut pas être vide") + .max(1024, "RowKey ne peut pas dépasser 1024 caractères") + .regex(azureKeyRegex, "RowKey ne peut pas contenir les caractères /, \\, #, ?") + .describe("Clé de ligne de l'entité"), + entity: z.record(AzureTableValueSchema) + .refine(data => { + // Vérifier qu'il n'y a pas de propriétés réservées + const reservedProps = ['PartitionKey', 'RowKey', 'Timestamp', 'ETag']; + const hasReserved = Object.keys(data).some(key => reservedProps.includes(key)); + return !hasReserved; + }, "L'entité ne peut pas contenir les propriétés réservées : PartitionKey, RowKey, Timestamp, ETag") + .refine(data => Object.keys(data).length <= 252, "Une entité ne peut pas avoir plus de 252 propriétés") + .describe("Données de l'entité à créer (max 252 propriétés, types autorisés : string, number, boolean, date, binary, null)"), +}); + +export type CreateEntityToolParams = z.infer; + +// Schema pour mettre à jour une entité +export const UpdateEntityToolSchema = z.object({ + tableName: z.string() + .regex(azureTableNameRegex, "Le nom de table doit commencer par une lettre et contenir seulement des lettres/chiffres (3-63 caractères)") + .describe("Nom de la table Azure"), + partitionKey: z.string() + .min(1, "PartitionKey ne peut pas être vide") + .max(1024, "PartitionKey ne peut pas dépasser 1024 caractères") + .regex(azureKeyRegex, "PartitionKey ne peut pas contenir les caractères /, \\, #, ?") + .describe("Clé de partition de l'entité"), + rowKey: z.string() + .min(1, "RowKey ne peut pas être vide") + .max(1024, "RowKey ne peut pas dépasser 1024 caractères") + .regex(azureKeyRegex, "RowKey ne peut pas contenir les caractères /, \\, #, ?") + .describe("Clé de ligne de l'entité"), + entity: z.record(AzureTableValueSchema) + .refine(data => { + // Vérifier qu'il n'y a pas de propriétés réservées + const reservedProps = ['PartitionKey', 'RowKey', 'Timestamp', 'ETag']; + const hasReserved = Object.keys(data).some(key => reservedProps.includes(key)); + return !hasReserved; + }, "L'entité ne peut pas contenir les propriétés réservées : PartitionKey, RowKey, Timestamp, ETag") + .refine(data => Object.keys(data).length <= 252, "Une entité ne peut pas avoir plus de 252 propriétés") + .describe("Données de l'entité à mettre à jour (max 252 propriétés, types autorisés : string, number, boolean, date, binary, null)"), + mode: z.enum(["merge", "replace"]).optional().default("merge").describe("Mode de mise à jour : merge ou replace"), +}); + +export type UpdateEntityToolParams = z.infer; + +// Schema pour supprimer une entité +export const DeleteEntityToolSchema = z.object({ + tableName: z.string() + .regex(azureTableNameRegex, "Le nom de table doit commencer par une lettre et contenir seulement des lettres/chiffres (3-63 caractères)") + .describe("Nom de la table Azure"), + partitionKey: z.string() + .min(1, "PartitionKey ne peut pas être vide") + .max(1024, "PartitionKey ne peut pas dépasser 1024 caractères") + .regex(azureKeyRegex, "PartitionKey ne peut pas contenir les caractères /, \\, #, ?") + .describe("Clé de partition de l'entité"), + rowKey: z.string() + .min(1, "RowKey ne peut pas être vide") + .max(1024, "RowKey ne peut pas dépasser 1024 caractères") + .regex(azureKeyRegex, "RowKey ne peut pas contenir les caractères /, \\, #, ?") + .describe("Clé de ligne de l'entité"), +}); + +export type DeleteEntityToolParams = z.infer; + +// Schema pour les opérations batch +export const BatchCreateEntitiesSchema = z.object({ + tableName: z.string() + .regex(azureTableNameRegex, "Le nom de table doit commencer par une lettre et contenir seulement des lettres/chiffres (3-63 caractères)") + .describe("Nom de la table Azure"), + entities: z.array(z.object({ + partitionKey: z.string() + .min(1, "PartitionKey ne peut pas être vide") + .max(1024, "PartitionKey ne peut pas dépasser 1024 caractères") + .regex(azureKeyRegex, "PartitionKey ne peut pas contenir les caractères /, \\, #, ?"), + rowKey: z.string() + .min(1, "RowKey ne peut pas être vide") + .max(1024, "RowKey ne peut pas dépasser 1024 caractères") + .regex(azureKeyRegex, "RowKey ne peut pas contenir les caractères /, \\, #, ?"), + entity: z.record(AzureTableValueSchema) + .refine(data => { + const reservedProps = ['PartitionKey', 'RowKey', 'Timestamp', 'ETag']; + const hasReserved = Object.keys(data).some(key => reservedProps.includes(key)); + return !hasReserved; + }, "L'entité ne peut pas contenir les propriétés réservées") + .refine(data => Object.keys(data).length <= 252, "Une entité ne peut pas avoir plus de 252 propriétés") + })) + .min(1, "Au moins une entité est requise") + .max(100, "Maximum 100 entités par batch (limitation Azure)") + .describe("Liste des entités à créer"), +}); + +export type BatchCreateEntitiesParams = z.infer; + +export const BatchUpdateEntitiesSchema = z.object({ + tableName: z.string() + .regex(azureTableNameRegex, "Le nom de table doit commencer par une lettre et contenir seulement des lettres/chiffres (3-63 caractères)") + .describe("Nom de la table Azure"), + entities: z.array(z.object({ + partitionKey: z.string() + .min(1, "PartitionKey ne peut pas être vide") + .max(1024, "PartitionKey ne peut pas dépasser 1024 caractères") + .regex(azureKeyRegex, "PartitionKey ne peut pas contenir les caractères /, \\, #, ?"), + rowKey: z.string() + .min(1, "RowKey ne peut pas être vide") + .max(1024, "RowKey ne peut pas dépasser 1024 caractères") + .regex(azureKeyRegex, "RowKey ne peut pas contenir les caractères /, \\, #, ?"), + entity: z.record(AzureTableValueSchema) + .refine(data => { + const reservedProps = ['PartitionKey', 'RowKey', 'Timestamp', 'ETag']; + const hasReserved = Object.keys(data).some(key => reservedProps.includes(key)); + return !hasReserved; + }, "L'entité ne peut pas contenir les propriétés réservées") + .refine(data => Object.keys(data).length <= 252, "Une entité ne peut pas avoir plus de 252 propriétés"), + mode: z.enum(["merge", "replace"]).optional().default("merge") + })) + .min(1, "Au moins une entité est requise") + .max(100, "Maximum 100 entités par batch (limitation Azure)") + .describe("Liste des entités à mettre à jour"), +}); + +export type BatchUpdateEntitiesParams = z.infer; + +export const BatchDeleteEntitiesSchema = z.object({ + tableName: z.string() + .regex(azureTableNameRegex, "Le nom de table doit commencer par une lettre et contenir seulement des lettres/chiffres (3-63 caractères)") + .describe("Nom de la table Azure"), + entities: z.array(z.object({ + partitionKey: z.string() + .min(1, "PartitionKey ne peut pas être vide") + .max(1024, "PartitionKey ne peut pas dépasser 1024 caractères") + .regex(azureKeyRegex, "PartitionKey ne peut pas contenir les caractères /, \\, #, ?"), + rowKey: z.string() + .min(1, "RowKey ne peut pas être vide") + .max(1024, "RowKey ne peut pas dépasser 1024 caractères") + .regex(azureKeyRegex, "RowKey ne peut pas contenir les caractères /, \\, #, ?") + })) + .min(1, "Au moins une entité est requise") + .max(100, "Maximum 100 entités par batch (limitation Azure)") + .describe("Liste des entités à supprimer"), +}); + +export type BatchDeleteEntitiesParams = z.infer; + +// Schema pour la gestion des tables +export const CreateTableSchema = z.object({ + tableName: z.string() + .regex(azureTableNameRegex, "Le nom de table doit commencer par une lettre et contenir seulement des lettres/chiffres (3-63 caractères)") + .describe("Nom de la table Azure à créer"), +}); + +export type CreateTableParams = z.infer; + +export const DeleteTableSchema = z.object({ + tableName: z.string() + .regex(azureTableNameRegex, "Le nom de table doit commencer par une lettre et contenir seulement des lettres/chiffres (3-63 caractères)") + .describe("Nom de la table Azure à supprimer"), +}); + +export type DeleteTableParams = z.infer; + +// Azure Blob Storage schemas +const azureContainerNameRegex = /^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])?$/; +const azureBlobNameRegex = /^[^\\/]+$/; + +// Schema pour les paramètres de connexion Azure Blob +export const AzureBlobConfigSchema = z.object({ + accountName: z.string().optional(), + connectionString: z.string().optional(), +}).refine(data => data.accountName || data.connectionString, { + message: "Either accountName or connectionString must be provided" +}); + +export type AzureBlobConfig = z.infer; + +// Schema pour créer un container +export const CreateContainerSchema = z.object({ + containerName: z.string() + .min(3, "Le nom du container doit contenir au moins 3 caractères") + .max(63, "Le nom du container ne peut pas dépasser 63 caractères") + .regex(azureContainerNameRegex, "Le nom du container doit contenir seulement des lettres minuscules, chiffres et tirets") + .describe("Nom du container à créer"), + publicAccess: z.enum(["container", "blob"]).optional().describe("Niveau d'accès public (container ou blob)"), +}); + +export type CreateContainerParams = z.infer; + +// Schema pour supprimer un container +export const DeleteContainerSchema = z.object({ + containerName: z.string() + .regex(azureContainerNameRegex, "Nom de container invalide") + .describe("Nom du container à supprimer"), +}); + +export type DeleteContainerParams = z.infer; + +// Schema pour lister les blobs +export const ListBlobsSchema = z.object({ + containerName: z.string() + .regex(azureContainerNameRegex, "Nom de container invalide") + .describe("Nom du container"), + prefix: z.string().optional().describe("Préfixe pour filtrer les blobs"), +}); + +export type ListBlobsParams = z.infer; + +// Schema pour uploader un blob +export const UploadBlobSchema = z.object({ + containerName: z.string() + .regex(azureContainerNameRegex, "Nom de container invalide") + .describe("Nom du container"), + blobName: z.string() + .min(1, "Le nom du blob ne peut pas être vide") + .max(1024, "Le nom du blob ne peut pas dépasser 1024 caractères") + .describe("Nom du blob à uploader"), + content: z.string().describe("Contenu du blob (texte ou base64)"), + contentType: z.string().optional().describe("Type MIME du contenu"), + metadata: z.record(z.string()).optional().describe("Métadonnées du blob"), + overwrite: z.boolean().optional().default(false).describe("Remplacer le blob s'il existe déjà"), +}); + +export type UploadBlobParams = z.infer; + +// Schema pour télécharger un blob +export const DownloadBlobSchema = z.object({ + containerName: z.string() + .regex(azureContainerNameRegex, "Nom de container invalide") + .describe("Nom du container"), + blobName: z.string() + .min(1, "Le nom du blob ne peut pas être vide") + .describe("Nom du blob à télécharger"), +}); + +export type DownloadBlobParams = z.infer; + +// Schema pour supprimer un blob +export const DeleteBlobSchema = z.object({ + containerName: z.string() + .regex(azureContainerNameRegex, "Nom de container invalide") + .describe("Nom du container"), + blobName: z.string() + .min(1, "Le nom du blob ne peut pas être vide") + .describe("Nom du blob à supprimer"), +}); + +export type DeleteBlobParams = z.infer; + +// Schema pour obtenir les propriétés d'un blob +export const GetBlobPropertiesSchema = z.object({ + containerName: z.string() + .regex(azureContainerNameRegex, "Nom de container invalide") + .describe("Nom du container"), + blobName: z.string() + .min(1, "Le nom du blob ne peut pas être vide") + .describe("Nom du blob"), +}); + +export type GetBlobPropertiesParams = z.infer; + +// Azure Service Bus Queue schemas +const azureQueueNameRegex = /^[a-zA-Z0-9]([a-zA-Z0-9\-._]){0,258}[a-zA-Z0-9]$/; + +// Schema pour les paramètres de connexion Azure Service Bus +export const AzureQueueConfigSchema = z.object({ + namespaceName: z.string().optional(), + connectionString: z.string().optional(), +}).refine(data => data.namespaceName || data.connectionString, { + message: "Either namespaceName or connectionString must be provided" +}); + +export type AzureQueueConfig = z.infer; + +// Schema pour créer une queue +export const CreateQueueSchema = z.object({ + queueName: z.string() + .min(1, "Le nom de la queue ne peut pas être vide") + .max(260, "Le nom de la queue ne peut pas dépasser 260 caractères") + .regex(azureQueueNameRegex, "Le nom de la queue doit commencer et finir par une lettre/chiffre, peut contenir lettres, chiffres, tirets, points et underscores") + .describe("Nom de la queue à créer"), + maxSizeInMegabytes: z.number().min(1).max(5120).optional().describe("Taille max en MB (1-5120)"), + defaultMessageTimeToLive: z.string().optional().describe("TTL par défaut des messages (format ISO 8601, ex: P14D pour 14 jours)"), + lockDuration: z.string().optional().describe("Durée de verrouillage des messages (format ISO 8601, ex: PT30S pour 30 secondes)"), + requiresDuplicateDetection: z.boolean().optional().describe("Activer la détection de doublons"), + requiresSession: z.boolean().optional().describe("Requiert des sessions"), + deadLetteringOnMessageExpiration: z.boolean().optional().describe("Activer dead letter sur expiration"), +}); + +export type CreateQueueParams = z.infer; + +// Schema pour supprimer une queue +export const DeleteQueueSchema = z.object({ + queueName: z.string() + .regex(azureQueueNameRegex, "Nom de queue invalide") + .describe("Nom de la queue à supprimer"), +}); + +export type DeleteQueueParams = z.infer; + +// Schema pour envoyer un message +export const SendMessageSchema = z.object({ + queueName: z.string() + .regex(azureQueueNameRegex, "Nom de queue invalide") + .describe("Nom de la queue"), + messageBody: z.string() + .min(1, "Le corps du message ne peut pas être vide") + .describe("Corps du message à envoyer"), + messageId: z.string().optional().describe("ID unique du message"), + correlationId: z.string().optional().describe("ID de corrélation"), + label: z.string().optional().describe("Label/sujet du message"), + timeToLive: z.number().optional().describe("TTL du message en millisecondes"), + sessionId: z.string().optional().describe("ID de session (si sessions activées)"), + userProperties: z.record(z.any()).optional().describe("Propriétés personnalisées du message"), +}); + +export type SendMessageParams = z.infer; + +// Schema pour recevoir des messages +export const ReceiveMessageSchema = z.object({ + queueName: z.string() + .regex(azureQueueNameRegex, "Nom de queue invalide") + .describe("Nom de la queue"), + maxMessageCount: z.number().min(1).max(100).optional().describe("Nombre max de messages à recevoir (1-100)"), + maxWaitTimeInMs: z.number().min(1).max(300000).optional().describe("Temps d'attente max en ms (1-300000)"), +}); + +export type ReceiveMessageParams = z.infer; + +// Schema pour aperçu des messages +export const PeekMessageSchema = z.object({ + queueName: z.string() + .regex(azureQueueNameRegex, "Nom de queue invalide") + .describe("Nom de la queue"), + maxMessageCount: z.number().min(1).max(100).optional().describe("Nombre max de messages à apercevoir (1-100)"), +}); + +export type PeekMessageParams = z.infer; + +// Schema pour obtenir les propriétés d'une queue +export const GetQueuePropertiesSchema = z.object({ + queueName: z.string() + .regex(azureQueueNameRegex, "Nom de queue invalide") + .describe("Nom de la queue"), +}); + +export type GetQueuePropertiesParams = z.infer; + +// Azure Storage Queue schemas (différent des Service Bus Queues) +const azureStorageQueueNameRegex = /^[a-z0-9]([a-z0-9-]){1,61}[a-z0-9]$/; + +// Schema pour les paramètres de connexion Azure Storage Queue +export const AzureStorageQueueConfigSchema = z.object({ + accountName: z.string().optional(), + connectionString: z.string().optional(), +}).refine(data => data.accountName || data.connectionString, { + message: "Either accountName or connectionString must be provided" +}); + +export type AzureStorageQueueConfig = z.infer; + +// Schema pour créer une queue storage +export const CreateStorageQueueSchema = z.object({ + queueName: z.string() + .min(3, "Le nom de la queue doit contenir au moins 3 caractères") + .max(63, "Le nom de la queue ne peut pas dépasser 63 caractères") + .regex(azureStorageQueueNameRegex, "Le nom de la queue doit contenir seulement des lettres minuscules, chiffres et tirets") + .describe("Nom de la queue à créer"), + metadata: z.record(z.string()).optional().describe("Métadonnées de la queue"), +}); + +export type CreateStorageQueueParams = z.infer; + +// Schema pour supprimer une queue storage +export const DeleteStorageQueueSchema = z.object({ + queueName: z.string() + .regex(azureStorageQueueNameRegex, "Nom de queue invalide") + .describe("Nom de la queue à supprimer"), +}); + +export type DeleteStorageQueueParams = z.infer; + +// Schema pour envoyer un message storage +export const SendStorageMessageSchema = z.object({ + queueName: z.string() + .regex(azureStorageQueueNameRegex, "Nom de queue invalide") + .describe("Nom de la queue"), + messageText: z.string() + .min(1, "Le texte du message ne peut pas être vide") + .max(65536, "Le message ne peut pas dépasser 64KB") + .describe("Texte du message à envoyer"), + visibilityTimeoutInSeconds: z.number().min(1).max(604800).optional().describe("Délai de visibilité en secondes (1-604800)"), + messageTimeToLiveInSeconds: z.number().min(1).max(604800).optional().describe("TTL du message en secondes (1-604800)"), +}); + +export type SendStorageMessageParams = z.infer; + +// Schema pour recevoir des messages storage +export const ReceiveStorageMessagesSchema = z.object({ + queueName: z.string() + .regex(azureStorageQueueNameRegex, "Nom de queue invalide") + .describe("Nom de la queue"), + numberOfMessages: z.number().min(1).max(32).optional().describe("Nombre de messages à recevoir (1-32)"), + visibilityTimeoutInSeconds: z.number().min(1).max(43200).optional().describe("Délai de visibilité en secondes (1-43200)"), +}); + +export type ReceiveStorageMessagesParams = z.infer; + +// Schema pour aperçu des messages storage +export const PeekStorageMessagesSchema = z.object({ + queueName: z.string() + .regex(azureStorageQueueNameRegex, "Nom de queue invalide") + .describe("Nom de la queue"), + numberOfMessages: z.number().min(1).max(32).optional().describe("Nombre de messages à apercevoir (1-32)"), +}); + +export type PeekStorageMessagesParams = z.infer; + +// Schema pour supprimer un message storage +export const DeleteStorageMessageSchema = z.object({ + queueName: z.string() + .regex(azureStorageQueueNameRegex, "Nom de queue invalide") + .describe("Nom de la queue"), + messageId: z.string().describe("ID du message à supprimer"), + popReceipt: z.string().describe("Pop receipt du message"), +}); + +export type DeleteStorageMessageParams = z.infer; + +// Schema pour obtenir les propriétés d'une queue storage +export const GetStorageQueuePropertiesSchema = z.object({ + queueName: z.string() + .regex(azureStorageQueueNameRegex, "Nom de queue invalide") + .describe("Nom de la queue"), +}); + +export type GetStorageQueuePropertiesParams = z.infer; + +// Schema pour vider une queue storage +export const ClearStorageQueueSchema = z.object({ + queueName: z.string() + .regex(azureStorageQueueNameRegex, "Nom de queue invalide") + .describe("Nom de la queue à vider"), +}); + +export type ClearStorageQueueParams = z.infer; \ No newline at end of file From 14c7ac080ecb290a763456148ed436779ef68735 Mon Sep 17 00:00:00 2001 From: salimlaimeche Date: Fri, 25 Jul 2025 19:19:55 +0200 Subject: [PATCH 2/2] get retrieval tools, find index and statistic --- azure-ai-search/.env.example | 6 + azure-ai-search/CLAUDE.md | 123 +++ azure-ai-search/ROADMAP.md | 181 +++++ azure-ai-search/TESTS_PHASE4.md | 150 ++++ azure-ai-search/lib/azure-search-client.ts | 662 ++++++++++++++++ azure-ai-search/manual-test.js | 106 +++ azure-ai-search/package.json | 1 + azure-ai-search/pnpm-lock.yaml | 48 ++ azure-ai-search/quick-test.mjs | 64 ++ azure-ai-search/resources/index-resources.ts | 103 +++ azure-ai-search/rollup.config.js | 7 +- azure-ai-search/server.ts | 456 ++++++++++- azure-ai-search/test-phase4.js | 82 ++ azure-ai-search/tools/document-tools.ts | 109 +++ azure-ai-search/tools/index-tools.ts | 47 ++ azure-ai-search/tools/search-tools.ts | 41 + azure-ai-search/tools/vector-tools.ts | 50 ++ azure-ai-search/types.ts | 769 +++++++------------ 18 files changed, 2487 insertions(+), 518 deletions(-) create mode 100644 azure-ai-search/ROADMAP.md create mode 100644 azure-ai-search/TESTS_PHASE4.md create mode 100644 azure-ai-search/lib/azure-search-client.ts create mode 100644 azure-ai-search/manual-test.js create mode 100644 azure-ai-search/quick-test.mjs create mode 100644 azure-ai-search/resources/index-resources.ts create mode 100644 azure-ai-search/test-phase4.js create mode 100644 azure-ai-search/tools/document-tools.ts create mode 100644 azure-ai-search/tools/index-tools.ts create mode 100644 azure-ai-search/tools/search-tools.ts create mode 100644 azure-ai-search/tools/vector-tools.ts diff --git a/azure-ai-search/.env.example b/azure-ai-search/.env.example index e69de29..ad2f609 100644 --- a/azure-ai-search/.env.example +++ b/azure-ai-search/.env.example @@ -0,0 +1,6 @@ +# Azure Search Configuration +AZURE_SEARCH_ENDPOINT=--your-azure-search-endpoint-- +AZURE_SEARCH_API_KEY=--your-azure-search-api-key-- + +# Alternative: Using Managed Identity (set service name instead of API key) +# AZURE_SEARCH_SERVICE_NAME=your-search-service diff --git a/azure-ai-search/CLAUDE.md b/azure-ai-search/CLAUDE.md index e69de29..4a9e966 100644 --- a/azure-ai-search/CLAUDE.md +++ b/azure-ai-search/CLAUDE.md @@ -0,0 +1,123 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +- **Build**: `pnpm build` - Compiles TypeScript to JavaScript using Rollup +- **Start Development**: `pnpm start` - Runs the server directly with ts-node +- **Start Production**: `pnpm start:prod` - Runs the built server from dist/ +- **Inspect**: `pnpm inspect` - Runs the MCP inspector for debugging + +## Architecture Overview + +This is an MCP (Model Context Protocol) server that provides Azure AI Search integration. Currently implements Phase 1 of the roadmap focusing on document retrieval and search functionality. + +### Current Implementation Status + +** Phase 1.1 - Configuration & Authentication** +- Azure Search Documents SDK integrated (`@azure/search-documents`) +- Dual authentication support (API key + Managed Identity) +- Environment variables: `AZURE_SEARCH_ENDPOINT`, `AZURE_SEARCH_API_KEY` + +** Phase 1.2 - Core Search Tools (Retrieval)** +- `search-documents` - Full-text search with filtering, faceting, highlighting +- `get-document` - Retrieve specific document by key +- `suggest` - Search suggestions using configured suggesters +- `autocomplete` - Auto-completion for partial search terms + +**✅ Phase 2.1 - Index Management & Discovery** +- `list-indexes` - List all available search indexes +- `get-index-schema` - Get complete index schema and field definitions +- `get-index-statistics` - Get index usage statistics and document counts + +**✅ Phase 2.2 - Dynamic Resources** +- Auto-discovery of available indexes at startup +- Dynamic resources for each index: schema, statistics, sample documents +- Resource URIs: `azure-search://indexes`, `azure-search://index/{name}/schema`, etc. + +**✅ Phase 3.1 - Document Management (COMPLETE)** +- `upload-documents` - Upload/create documents (batch operations up to 1000) +- `merge-documents` - Partial update of existing documents +- `delete-documents` - Delete documents by key values (batch operations) + +**✅ Phase 4.1 - Vector Search (COMPLETE)** +- `vector-search` - Pure vector similarity search using k-nearest neighbors +- `hybrid-search` - Combined text and vector search for enhanced relevance +- Support for multiple vector queries and exhaustive search modes + +**✅ Phase 4.2 - Semantic Search (COMPLETE)** +- `semantic-search` - Azure AI semantic search with natural language understanding +- Semantic answers extraction from search results +- Semantic captions with highlighting support +- Integration with Azure's semantic configurations + +### Core Components + +- **server.ts** - Main MCP server entry point with tool registration +- **lib/azure-search-client.ts** - Azure Search client wrapper with error handling +- **tools/search-tools.ts** - Search tool implementations with validation +- **tools/index-tools.ts** - Index management tool implementations +- **resources/index-resources.ts** - Dynamic resource registration for discovered indexes +- **types.ts** - Zod schemas for Azure AI Search parameters and responses + +### Key Architecture Patterns + +1. **Lazy Loading**: Azure Search clients instantiated only when first accessed +2. **Client Caching**: Search clients cached per index name for efficiency +3. **Dual Authentication**: Supports both API key and DefaultAzureCredential +4. **Type Safety**: All parameters validated with Zod schemas +5. **Error Handling**: Consistent success/error response format + +### Configuration + +#### Environment Variables +```env +AZURE_SEARCH_ENDPOINT=https://your-service.search.windows.net +AZURE_SEARCH_API_KEY=your-api-key +``` + +#### Alternative: Managed Identity +```env +AZURE_SEARCH_ENDPOINT=https://your-service.search.windows.net +# No API key needed - uses DefaultAzureCredential +``` + +### Available Tools + +#### Core Search Tools +- **search-documents** - Full-text search with filtering, faceting, highlighting +- **get-document** - Retrieve specific document by primary key +- **suggest** - Search suggestions using configured suggester with fuzzy matching +- **autocomplete** - Auto-complete partial search terms with multiple modes + +#### Index Management Tools +- **list-indexes** - List all available search indexes +- **get-index-schema** - Get complete index schema and field definitions +- **get-index-statistics** - Get index usage statistics and document counts + +#### Document Management Tools +- **upload-documents** - Upload/create documents (batch operations up to 1000) +- **merge-documents** - Partial update of existing documents +- **delete-documents** - Delete documents by key values (batch operations) + +#### Vector Search Tools (Phase 4) +- **vector-search** - Pure vector similarity search using k-nearest neighbors +- **hybrid-search** - Combined text and vector search for enhanced relevance + +#### Semantic Search Tools (Phase 4) +- **semantic-search** - Azure AI semantic search with natural language understanding + +### Build System + +Uses Rollup with TypeScript compilation. External dependencies are not bundled to reduce size. Some TypeScript warnings exist but don't affect functionality. + +### Next Steps (Roadmap) + +See `ROADMAP.md` for complete implementation plan: +- ✅ **Phase 1**: Core search and retrieval (COMPLETE) +- ✅ **Phase 2**: Index management and discovery (COMPLETE) +- ✅ **Phase 3**: Document upload/management (COMPLETE) +- ✅ **Phase 4**: Vector and semantic search (COMPLETE) +- 🎯 **Phase 5**: Advanced index operations (create/update/delete indexes) +- 📊 **Phase 6**: Analytics and performance monitoring \ No newline at end of file diff --git a/azure-ai-search/ROADMAP.md b/azure-ai-search/ROADMAP.md new file mode 100644 index 0000000..2432943 --- /dev/null +++ b/azure-ai-search/ROADMAP.md @@ -0,0 +1,181 @@ +# Azure AI Search MCP - Roadmap + +## ✅ État Actuel - PHASES 1-4 COMPLÈTES ! +Le serveur MCP Azure AI Search est maintenant **fonctionnel et complet** pour toutes les opérations avancées : +- ✅ Search & Retrieval (Phase 1) +- ✅ Index Management & Discovery (Phase 2) +- ✅ Document Management (Phase 3) +- ✅ Vector & Semantic Search (Phase 4) + +--- + +## ✅ Phase 1: Foundation & Retrieval (COMPLÈTE) 🔍 + +### ✅ 1.1 Configuration & Authentication +- ✅ Dépendance `@azure/search-documents` ajoutée +- ✅ types.ts nettoyé avec schémas Zod pour Azure AI Search +- ✅ Authentication dual (API key + Managed Identity) +- ✅ Variables d'env: `AZURE_SEARCH_ENDPOINT`, `AZURE_SEARCH_API_KEY` + +### ✅ 1.2 Core Search Tools (Retrieval) +- ✅ **search-documents** - Recherche complète avec filtres, facettes, highlighting +- ✅ **get-document** - Récupération de document par clé +- ✅ **suggest** - Suggestions de recherche avec fuzzy matching +- ✅ **autocomplete** - Auto-complétion de termes + +### ✅ 1.3 Index Discovery Resources +- ✅ **list-indexes** - Liste tous les index disponibles +- ✅ **get-index-schema** - Récupère le schéma complet d'un index +- ✅ Resources dynamiques pour chaque index découvert + +--- + +## ✅ Phase 2: Index Management & Discovery (COMPLÈTE) ⚙️ + +### ✅ 2.1 Index Operations +- ✅ **get-index-statistics** - Statistiques et usage d'un index +- ✅ **get-index-schema** - Schéma détaillé avec fields, analyzers, etc. +- ✅ Dynamic resource registration au démarrage + +### ✅ 2.2 Dynamic Resources +- ✅ Auto-discovery des index au startup +- ✅ Resources MCP créées automatiquement : + - `azure-search://indexes` - Liste complète + - `azure-search://index/{name}/schema` - Schéma par index + - `azure-search://index/{name}/statistics` - Stats par index + - `azure-search://index/{name}/sample` - Documents échantillons + +--- + +## ✅ Phase 3: Document Management (COMPLÈTE) 📄 + +### ✅ 3.1 Document Operations +- ✅ **upload-documents** - Upload/création de documents (batch 1000 max) +- ✅ **merge-documents** - Mise à jour partielle de documents existants +- ✅ **delete-documents** - Suppression de documents par clés (batch 1000 max) + +### ✅ 3.2 Document Processing +- ✅ Validation complète des documents selon schémas Zod +- ✅ Gestion d'erreurs batch avec détails par document +- ✅ Support des types Azure AI Search (text, vector, etc.) + +## ✅ Phase 4: Vector & Semantic Search (COMPLÈTE) 🤖 + +### ✅ 4.1 Vector Search Enhancement +- ✅ **vector-search** - Recherche vectorielle native avec K-NN +- ✅ **hybrid-search** - Recherche hybride (text + vector) +- ✅ **knn-search** - Intégré dans vector-search (paramètre k) +- ✅ **vector-filtering** - Support des filtres OData sur résultats vectoriels + +### ✅ 4.2 Semantic Search +- ✅ **semantic-search** - Recherche sémantique Azure avec configuration +- ✅ **semantic-answers** - Réponses sémantiques extraites automatiquement +- ✅ **semantic-captions** - Légendes sémantiques avec highlighting +- ✅ **semantic-ranking** - Classement sémantique intégré + +## Phase 5: Advanced Index Operations (Utile) ⚙️ + +### 5.1 Index Lifecycle +- [ ] **create-index** - Création d'index avec schéma complet +- [ ] **update-index** - Mise à jour schéma d'index existant +- [ ] **delete-index** - Suppression d'index +- [ ] **index-aliases** - Gestion des alias d'index + +### 5.2 Skillsets & Enrichment +- [ ] **list-skillsets** - Liste des skillsets disponibles +- [ ] **get-skillset** - Détails d'un skillset +- [ ] **run-indexer** - Exécution d'un indexer +- [ ] **indexer-status** - Statut des indexers + +## Phase 6: Analytics & Performance (Optionnel) 📊 + +### 6.1 Search Analytics +- [ ] **search-analytics** - Métriques de recherche +- [ ] **query-performance** - Performance des requêtes +- [ ] **index-health** - Santé des index +- [ ] **usage-statistics** - Statistiques d'utilisation + +### 6.2 Monitoring Tools +- [ ] **connection-health** - Test de connectivité +- [ ] **quota-usage** - Utilisation des quotas +- [ ] **service-statistics** - Statistiques du service + +## Architecture Technique + +### Structure des Fichiers +``` +azure-ai-search/ +├── server.ts # Point d'entrée MCP +├── types.ts # Schémas Zod pour AI Search +├── lib/ +│ └── azure-search-client.ts # Client Azure AI Search +├── tools/ +│ ├── search-tools.ts # Outils de recherche +│ ├── index-tools.ts # Gestion des index +│ ├── document-tools.ts # Gestion des documents +│ └── analytics-tools.ts # Analytics (Phase 4) +├── resources/ +│ ├── index-resources.ts # Resources dynamiques des index +│ └── search-resources.ts # Resources de recherche +└── prompts/ + └── search-prompts.ts # Prompts pour la recherche +``` + +### Patterns Architecturaux +- **Lazy Loading**: Client Azure Search instancié à la demande +- **Dynamic Resources**: Découverte automatique des index +- **Error Handling**: Format de réponse cohérent avec success/error +- **Authentication**: Support API Key + Managed Identity +- **Validation**: Schémas Zod stricts pour tous les paramètres + +### Configuration Environnement +```env +AZURE_SEARCH_ENDPOINT=https://myservice.search.windows.net +AZURE_SEARCH_API_KEY=your-api-key +# OU pour Managed Identity: +AZURE_SEARCH_SERVICE_NAME=myservice +``` + +## 🎯 Recommandations pour la Suite + +### Phase 4 Prioritaire: Vector & Semantic Search +Le **Vector Search** est la prochaine étape logique car : +- **Tendance forte** dans l'IA générative et RAG +- **Value-add majeur** pour les applications d'IA +- **Déjà supporté** par Azure AI Search +- **Complémentaire** aux fonctionnalités existantes + +### Phase 5 Utile: Index Operations +Création et gestion d'index directement depuis MCP : +- **Workflow complet** de A à Z +- **Productivité** pour les développeurs +- **Gestion de cycle de vie** des index + +### Phase 6 Optionnel: Analytics +Monitoring et métriques pour optimisation : +- **Debug** et troubleshooting +- **Performance tuning** +- **Usage insights** + +## Priorités de Développement + +1. ✅ **Phase 1** (COMPLETE): Retrieval fonctionnel +2. ✅ **Phase 2** (COMPLETE): Gestion basique des index +3. ✅ **Phase 3** (COMPLETE): Gestion des documents +4. 🎯 **Phase 4** (Recommandé): Vector & Semantic Search +5. ⚙️ **Phase 5** (Utile): Advanced Index Operations +6. 📊 **Phase 6** (Optionnel): Analytics & Performance + +## Critères de Succès + +### Phase 1 (MVP) +- [ ] Recherche simple fonctionnelle sur index existants +- [ ] Auto-discovery des index disponibles +- [ ] Gestion d'erreurs robuste +- [ ] Documentation clara avec exemples + +### Phases Suivantes +- [ ] Gestion complète du cycle de vie des index +- [ ] Support des opérations batch performantes +- [ ] Intégration avec les outils d'IA générative +- [ ] Métriques et monitoring intégrés \ No newline at end of file diff --git a/azure-ai-search/TESTS_PHASE4.md b/azure-ai-search/TESTS_PHASE4.md new file mode 100644 index 0000000..304e728 --- /dev/null +++ b/azure-ai-search/TESTS_PHASE4.md @@ -0,0 +1,150 @@ +# Tests Phase 4 - Vector & Semantic Search +## Index Elite Dangerous RAG: `rag-1753386801239` + +Voici les tests à effectuer pour valider la Phase 4 sur ton index Elite Dangerous. + +## Configuration Index Découverte +- **Champ vector**: `text_vector` (1536 dimensions) +- **Config sémantique**: `rag-1753386801239-semantic-configuration` +- **Champs searchables**: `chunk`, `title`, `header_1`, `header_2`, `header_3` +- **Profil vectoriel**: `rag-1753386801239-azureOpenAi-text-profile` + +--- + +## Test 1: 📖 Search Documents Classique +**Tool**: `search-documents` +```json +{ + "indexName": "rag-1753386801239", + "searchText": "thargoid combat", + "top": 5, + "highlightFields": ["chunk", "title"], + "highlightPreTag": "", + "highlightPostTag": "" +} +``` + +**Résultat attendu**: Articles/guides sur le combat contre les Thargoids avec highlighting. + +--- + +## Test 2: 🔍 Vector Search +**Tool**: `vector-search` +```json +{ + "indexName": "rag-1753386801239", + "vectorQueries": [ + { + "vector": [0.001, 0.002, 0.001, ...répéter 1536 fois...], + "fields": "text_vector", + "k": 5, + "exhaustive": false + } + ], + "top": 5 +} +``` + +**Note**: Pour un vrai test, il faudrait un embedding généré par OpenAI text-embedding-3-small pour un concept comme "ship combat" ou "exploration". + +--- + +## Test 3: 🧠 Semantic Search avec Réponses +**Tool**: `semantic-search` +```json +{ + "indexName": "rag-1753386801239", + "searchText": "Comment combattre efficacement les Thargoids dans Elite Dangerous ?", + "semanticConfiguration": "rag-1753386801239-semantic-configuration", + "answers": { + "answerType": "extractive", + "count": 3, + "threshold": 0.7 + }, + "captions": { + "captionType": "extractive", + "maxTextRecordsToProcess": 1000, + "highlight": true + }, + "top": 5 +} +``` + +**Résultat attendu**: +- Réponses extraites directement du contenu +- Captions avec highlighting +- Ranking sémantique amélioré + +--- + +## Test 4: 🔥 Hybrid Search (Text + Vector) +**Tool**: `hybrid-search` +```json +{ + "indexName": "rag-1753386801239", + "searchText": "ship loadout", + "vectorQueries": [ + { + "vector": [... embedding pour "ship loadout" ...], + "fields": "text_vector", + "k": 10 + } + ], + "searchMode": "any", + "top": 5 +} +``` + +**Résultat attendu**: Combinaison optimale de pertinence textuelle et vectorielle. + +--- + +## Autres Tests Utiles + +### Test index management +```json +// Tool: list-indexes +{} + +// Tool: get-index-schema +{ + "indexName": "rag-1753386801239" +} +``` + +### Test recherche spécialisée Elite Dangerous +```json +// Tool: search-documents +{ + "indexName": "rag-1753386801239", + "searchText": "guardian technology ruins", + "searchFields": ["chunk", "title"], + "filter": null, + "top": 10 +} +``` + +--- + +## Instructions de Test + +1. **Lance l'inspecteur MCP** : `pnpm inspect` +2. **Ouvre** http://localhost:3000 dans le navigateur +3. **Teste chaque tool** avec les exemples ci-dessus +4. **Vérifie** les résultats pour : + - ✅ Pas d'erreurs de connexion + - ✅ Réponses bien formatées + - ✅ Contenu pertinent Elite Dangerous + - ✅ Nouvelles fonctionnalités (semantic answers, vector similarity) + +## Résultats Attendus + +### ✅ Phase 4 Validée Si: +- [ ] `search-documents` fonctionne avec highlighting +- [ ] `vector-search` s'exécute sans erreur (même avec dummy vector) +- [ ] `semantic-search` retourne des réponses et captions +- [ ] `hybrid-search` combine text et vector +- [ ] Tous les outils gèrent les erreurs proprement +- [ ] Performance acceptable (<2s par requête) + +**🎯 Objectif**: Valider que toutes les fonctionnalités Phase 4 (Vector & Semantic Search) sont opérationnelles sur ton index RAG Elite Dangerous ! \ No newline at end of file diff --git a/azure-ai-search/lib/azure-search-client.ts b/azure-ai-search/lib/azure-search-client.ts new file mode 100644 index 0000000..7828c8d --- /dev/null +++ b/azure-ai-search/lib/azure-search-client.ts @@ -0,0 +1,662 @@ +import { SearchClient, SearchIndexClient, AzureKeyCredential } from "@azure/search-documents"; +import { DefaultAzureCredential } from "@azure/identity"; +import dotenv from "dotenv"; +import type { + AzureSearchConfig, + SearchResult, + DocumentResult, + SuggestResult, + AutocompleteResult, + IndexResult, + BatchResult, + VectorSearchResult, + SemanticSearchResult, + VectorQuery +} from "../types.js"; + +dotenv.config(); + +export class AzureSearchTools { + private indexClient: SearchIndexClient | null = null; + private searchClients: Map> = new Map(); + private config: AzureSearchConfig; + + constructor(config?: Partial) { + this.config = { + endpoint: config?.endpoint || process.env.AZURE_SEARCH_ENDPOINT || "", + apiKey: config?.apiKey || process.env.AZURE_SEARCH_API_KEY, + apiVersion: config?.apiVersion || "2023-11-01", + }; + + if (!this.config.endpoint) { + throw new Error("Azure Search endpoint is required. Set AZURE_SEARCH_ENDPOINT environment variable or provide it in config."); + } + } + + private getIndexClient(): SearchIndexClient { + if (!this.indexClient) { + const credential = this.config.apiKey + ? new AzureKeyCredential(this.config.apiKey) + : new DefaultAzureCredential(); + + this.indexClient = new SearchIndexClient(this.config.endpoint, credential); + } + return this.indexClient; + } + + private getSearchClient(indexName: string): SearchClient { + if (!this.searchClients.has(indexName)) { + const credential = this.config.apiKey + ? new AzureKeyCredential(this.config.apiKey) + : new DefaultAzureCredential(); + + const client = new SearchClient(this.config.endpoint, indexName, credential); + this.searchClients.set(indexName, client); + } + return this.searchClients.get(indexName)!; + } + + // Search Operations + async searchDocuments(params: { + indexName: string; + searchText: string; + searchMode?: "any" | "all"; + searchFields?: string[]; + select?: string[]; + filter?: string; + orderBy?: string[]; + top?: number; + skip?: number; + includeTotalCount?: boolean; + facets?: string[]; + highlightFields?: string[]; + highlightPreTag?: string; + highlightPostTag?: string; + minimumCoverage?: number; + queryType?: "simple" | "full"; + }): Promise { + try { + const client = this.getSearchClient(params.indexName); + + const searchOptions: any = { + searchMode: params.searchMode, + searchFields: params.searchFields, + select: params.select, + filter: params.filter, + orderBy: params.orderBy, + top: params.top, + skip: params.skip, + includeTotalCount: params.includeTotalCount, + facets: params.facets, + highlightFields: params.highlightFields?.join(','), // Convert array to comma-separated string + highlightPreTag: params.highlightPreTag, + highlightPostTag: params.highlightPostTag, + minimumCoverage: params.minimumCoverage, + queryType: params.queryType, + }; + + const response = await client.search(params.searchText, searchOptions); + + const results = []; + for await (const result of response.results) { + results.push(result); + } + + return { + success: true, + data: { + results, + count: response.count, + facets: response.facets, + coverage: response.coverage, + // nextPageParameters: response.nextPageParameters, // Property doesn't exist in current API + }, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error occurred", + }; + } + } + + async getDocument(params: { + indexName: string; + key: string; + select?: string[]; + }): Promise { + try { + const client = this.getSearchClient(params.indexName); + + const result = await client.getDocument(params.key, { + selectedFields: params.select, + }); + + return { + success: true, + data: result, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error occurred", + }; + } + } + + async suggest(params: { + indexName: string; + searchText: string; + suggesterName: string; + fuzzy?: boolean; + highlightPreTag?: string; + highlightPostTag?: string; + minimumCoverage?: number; + orderBy?: string[]; + searchFields?: string[]; + select?: string[]; + top?: number; + filter?: string; + }): Promise { + try { + const client = this.getSearchClient(params.indexName); + + const response = await client.suggest(params.searchText, params.suggesterName, { + useFuzzyMatching: params.fuzzy, + highlightPreTag: params.highlightPreTag, + highlightPostTag: params.highlightPostTag, + minimumCoverage: params.minimumCoverage, + orderBy: params.orderBy, + searchFields: params.searchFields, + select: params.select, + top: params.top, + filter: params.filter, + }); + + return { + success: true, + data: { + results: response.results.map(r => ({ + text: r.text, + document: r.document, + })), + coverage: response.coverage, + }, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error occurred", + }; + } + } + + async autocomplete(params: { + indexName: string; + searchText: string; + suggesterName: string; + autocompleteMode?: "oneTerm" | "twoTerms" | "oneTermWithContext"; + fuzzy?: boolean; + highlightPreTag?: string; + highlightPostTag?: string; + minimumCoverage?: number; + searchFields?: string[]; + top?: number; + filter?: string; + }): Promise { + try { + const client = this.getSearchClient(params.indexName); + + const response = await client.autocomplete(params.searchText, params.suggesterName, { + autocompleteMode: params.autocompleteMode, + useFuzzyMatching: params.fuzzy, + highlightPreTag: params.highlightPreTag, + highlightPostTag: params.highlightPostTag, + minimumCoverage: params.minimumCoverage, + searchFields: params.searchFields, + top: params.top, + filter: params.filter, + }); + + return { + success: true, + data: { + results: response.results.map(r => ({ + text: r.text, + queryPlusText: r.queryPlusText, + })), + coverage: response.coverage, + }, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error occurred", + }; + } + } + + // Index Management + async listIndexes(params?: { select?: string[] }): Promise { + try { + console.error("=== DEBUG: Starting listIndexes ==="); + console.error("Config:", { + endpoint: this.config.endpoint, + hasApiKey: !!this.config.apiKey, + apiKeyPrefix: this.config.apiKey?.substring(0, 10) + "..." + }); + + const client = this.getIndexClient(); + console.error("=== DEBUG: IndexClient created successfully ==="); + + const response = await client.listIndexes(); + console.error("=== DEBUG: listIndexes response received ==="); + console.error("Response type:", typeof response); + console.error("Response constructor:", response.constructor.name); + + const indexes = []; + let count = 0; + for await (const index of response) { + count++; + console.error(`=== DEBUG: Processing index #${count} ===`); + console.error("Index type:", typeof index); + console.error("Index keys:", Object.keys(index)); + console.error("Index name:", index.name); + console.error("Raw index object:", JSON.stringify(index, null, 2)); + + if (params?.select && params.select.length > 0) { + const filteredIndex: any = {}; + for (const field of params.select) { + if (field in index) { + filteredIndex[field] = (index as any)[field]; + } + } + indexes.push(filteredIndex); + } else { + // Debug: Include raw data in response for troubleshooting + const debugInfo = { + indexType: typeof index, + indexKeys: Object.keys(index), + indexName: index.name, + indexConstructor: index.constructor?.name + }; + + // Try multiple approaches to get the data + let simplifiedIndex; + + try { + // Approach 1: Manual property extraction + simplifiedIndex = { + _debug: debugInfo, + name: index.name || "unknown", + etag: index.etag || null, + fieldsCount: (index.fields?.length) || 0, + fields: index.fields ? index.fields.map((field: any) => ({ + name: field.name || "unknown", + type: field.type || "unknown", + key: Boolean(field.key), + searchable: Boolean(field.searchable), + filterable: Boolean(field.filterable), + facetable: Boolean(field.facetable), + sortable: Boolean(field.sortable), + vectorSearchDimensions: field.vectorSearchDimensions || null + })) : [], + suggesters: index.suggesters ? index.suggesters.map((s: any) => ({ + name: s.name || "unknown", + searchMode: s.searchMode || null, + sourceFields: s.sourceFields || [] + })) : [], + scoringProfiles: index.scoringProfiles ? index.scoringProfiles.map((sp: any) => sp.name || "unknown") : [], + hasSemanticSearch: Boolean(index.semanticSearch?.configurations?.length), + hasVectorSearch: Boolean(index.vectorSearch?.profiles?.length), + corsOptions: index.corsOptions ? { + allowedOrigins: index.corsOptions.allowedOrigins || [], + maxAgeInSeconds: index.corsOptions.maxAgeInSeconds || 0 + } : null + }; + } catch (error) { + // Fallback: just return basic info + simplifiedIndex = { + _error: "Failed to parse index object", + _debug: debugInfo, + _rawIndex: JSON.stringify(index, null, 2).substring(0, 500) + "..." + }; + } + + indexes.push(simplifiedIndex); + } + } + + console.error(`=== DEBUG: Processed ${count} indexes total ===`); + console.error("Final indexes array length:", indexes.length); + + return { + success: true, + data: indexes, + }; + } catch (error) { + console.error("=== DEBUG: Error in listIndexes ==="); + console.error("Error:", error); + console.error("Error message:", error instanceof Error ? error.message : "Unknown error"); + console.error("Error stack:", error instanceof Error ? error.stack : "No stack"); + + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error occurred", + }; + } + } + + async getIndex(indexName: string): Promise { + try { + const client = this.getIndexClient(); + const index = await client.getIndex(indexName); + + // Return a clean, serializable version of the index + const cleanIndex = JSON.parse(JSON.stringify(index)); + + return { + success: true, + data: cleanIndex, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error occurred", + }; + } + } + + async getIndexStatistics(indexName: string): Promise { + try { + const client = this.getIndexClient(); + const stats = await client.getIndexStatistics(indexName); + + return { + success: true, + data: stats, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error occurred", + }; + } + } + + // Document Management + async uploadDocuments(params: { + indexName: string; + documents: any[]; + }): Promise { + try { + const client = this.getSearchClient(params.indexName); + const response = await client.uploadDocuments(params.documents); + + return { + success: true, + data: { + results: response.results.map(r => ({ + key: r.key, + status: r.succeeded, + errorMessage: r.errorMessage, + })), + }, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error occurred", + }; + } + } + + async mergeDocuments(params: { + indexName: string; + documents: any[]; + }): Promise { + try { + const client = this.getSearchClient(params.indexName); + const response = await client.mergeDocuments(params.documents); + + return { + success: true, + data: { + results: response.results.map(r => ({ + key: r.key, + status: r.succeeded, + errorMessage: r.errorMessage, + })), + }, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error occurred", + }; + } + } + + async deleteDocuments(params: { + indexName: string; + keyField: string; + keyValues: string[]; + }): Promise { + try { + const client = this.getSearchClient(params.indexName); + + // Convert key values to documents for deletion + const documentsToDelete = params.keyValues.map(key => ({ + [params.keyField]: key, + })); + + const response = await client.deleteDocuments(documentsToDelete); + + return { + success: true, + data: { + results: response.results.map(r => ({ + key: r.key, + status: r.succeeded, + errorMessage: r.errorMessage, + })), + }, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error occurred", + }; + } + } + + // Phase 4: Vector Search Operations + async vectorSearch(params: { + indexName: string; + vectorQueries: VectorQuery[]; + select?: string[]; + filter?: string; + top?: number; + skip?: number; + }): Promise { + try { + const client = this.getSearchClient(params.indexName); + + const searchOptions: any = { + vectorQueries: params.vectorQueries.map(vq => ({ + vector: vq.vector, + fields: vq.fields, + k: vq.k, + exhaustive: vq.exhaustive, + })), + select: params.select, + filter: params.filter, + top: params.top, + skip: params.skip, + }; + + const response = await client.search("*", searchOptions); + + const results = []; + for await (const result of response.results) { + results.push(result); + } + + return { + success: true, + data: { + results, + count: response.count, + }, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error occurred", + }; + } + } + + async hybridSearch(params: { + indexName: string; + searchText: string; + vectorQueries: VectorQuery[]; + searchMode?: "any" | "all"; + searchFields?: string[]; + select?: string[]; + filter?: string; + orderBy?: string[]; + top?: number; + skip?: number; + queryType?: "simple" | "full"; + }): Promise { + try { + const client = this.getSearchClient(params.indexName); + + const searchOptions: any = { + searchMode: params.searchMode, + searchFields: params.searchFields, + vectorQueries: params.vectorQueries.map(vq => ({ + vector: vq.vector, + fields: vq.fields, + k: vq.k, + exhaustive: vq.exhaustive, + })), + select: params.select, + filter: params.filter, + orderBy: params.orderBy, + top: params.top, + skip: params.skip, + queryType: params.queryType, + }; + + const response = await client.search(params.searchText, searchOptions); + + const results = []; + for await (const result of response.results) { + results.push(result); + } + + return { + success: true, + data: { + results, + count: response.count, + facets: response.facets, + coverage: response.coverage, + }, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error occurred", + }; + } + } + + // Phase 4: Semantic Search Operations + async semanticSearch(params: { + indexName: string; + searchText: string; + semanticConfiguration: string; + searchFields?: string[]; + select?: string[]; + filter?: string; + orderBy?: string[]; + top?: number; + skip?: number; + answers?: { + answerType: "extractive"; + count?: number; + threshold?: number; + }; + captions?: { + captionType: "extractive"; + maxTextRecordsToProcess?: number; + highlight?: boolean; + }; + }): Promise { + try { + const client = this.getSearchClient(params.indexName); + + const searchOptions: any = { + queryType: "semantic", + semanticConfiguration: params.semanticConfiguration, + searchFields: params.searchFields, + select: params.select, + filter: params.filter, + orderBy: params.orderBy, + top: params.top, + skip: params.skip, + }; + + // Add semantic answers if configured + if (params.answers) { + searchOptions.answers = { + answerType: params.answers.answerType, + count: params.answers.count, + threshold: params.answers.threshold, + }; + } + + // Add semantic captions if configured + if (params.captions) { + searchOptions.captions = { + captionType: params.captions.captionType, + maxTextRecordsToProcess: params.captions.maxTextRecordsToProcess, + highlight: params.captions.highlight, + }; + } + + const response = await client.search(params.searchText, searchOptions); + + const results = []; + for await (const result of response.results) { + results.push(result); + } + + return { + success: true, + data: { + results, + count: response.count, + answers: (response as any).answers?.map((answer: any) => ({ + key: answer.key || "", + text: answer.text || "", + highlights: answer.highlights || "", + score: answer.score || 0, + })), + captions: (response as any).captions?.map((caption: any) => ({ + text: caption.text || "", + highlights: caption.highlights || "", + })), + }, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error occurred", + }; + } + } +} \ No newline at end of file diff --git a/azure-ai-search/manual-test.js b/azure-ai-search/manual-test.js new file mode 100644 index 0000000..e9f850f --- /dev/null +++ b/azure-ai-search/manual-test.js @@ -0,0 +1,106 @@ +#!/usr/bin/env node + +// Test manuel simple pour Phase 4 +import dotenv from 'dotenv'; +import { AzureSearchTools } from './lib/azure-search-client.js'; + +dotenv.config(); + +const tools = new AzureSearchTools(); +const indexName = "rag-1753386801239"; + +console.log("🚀 Test Phase 4 - Elite Dangerous RAG"); +console.log("=====================================\n"); + +// Test 1: Recherche classique Thargoids +console.log("📖 Test 1: Recherche classique 'thargoid'"); +try { + const result = await tools.searchDocuments({ + indexName, + searchText: "thargoid", + top: 2, + highlightFields: ["chunk", "title"] + }); + + if (result.success) { + console.log(`✅ Trouvé ${result.data.results.length} résultats`); + if (result.data.results.length > 0) { + const first = result.data.results[0]; + console.log(`📄 Premier: "${first.document?.title || 'Sans titre'}" (score: ${first.score || 'N/A'})`); + } + } else { + console.log("❌ Erreur:", result.error); + } +} catch (error) { + console.log("❌ Exception:", error.message); +} + +console.log("\n" + "=".repeat(50)); + +// Test 2: Recherche vectorielle avec un vecteur simulé +console.log("🔍 Test 2: Vector Search (simulé)"); +try { + // Vecteur aléatoire pour test (en production il faudrait un vrai embedding) + const dummyVector = Array(1536).fill(0).map(() => Math.random() * 0.001); + + const result = await tools.vectorSearch({ + indexName, + vectorQueries: [{ + vector: dummyVector, + fields: "text_vector", + k: 2 + }] + }); + + if (result.success) { + console.log(`✅ Vector search OK: ${result.data.results.length} résultats`); + if (result.data.results.length > 0) { + const first = result.data.results[0]; + console.log(`📄 Premier: "${first.document?.title || 'Sans titre'}" (score: ${first.score || 'N/A'})`); + } + } else { + console.log("❌ Vector search erreur:", result.error); + } +} catch (error) { + console.log("❌ Vector search exception:", error.message); +} + +console.log("\n" + "=".repeat(50)); + +// Test 3: Recherche sémantique +console.log("🧠 Test 3: Semantic Search"); +try { + const result = await tools.semanticSearch({ + indexName, + searchText: "Comment combattre les Thargoids efficacement ?", + semanticConfiguration: "rag-1753386801239-semantic-configuration", + answers: { + answerType: "extractive", + count: 2, + threshold: 0.6 + }, + captions: { + captionType: "extractive", + highlight: true + }, + top: 3 + }); + + if (result.success) { + console.log(`✅ Semantic search OK: ${result.data.results.length} résultats`); + console.log(`📝 Réponses extraites: ${result.data.answers?.length || 0}`); + console.log(`💬 Captions: ${result.data.captions?.length || 0}`); + + // Afficher la première réponse si disponible + if (result.data.answers && result.data.answers.length > 0) { + const answer = result.data.answers[0]; + console.log(`🎯 Première réponse (score ${answer.score}): ${answer.text.substring(0, 100)}...`); + } + } else { + console.log("❌ Semantic search erreur:", result.error); + } +} catch (error) { + console.log("❌ Semantic search exception:", error.message); +} + +console.log("\n✅ Tests Phase 4 terminés !"); \ No newline at end of file diff --git a/azure-ai-search/package.json b/azure-ai-search/package.json index bf9b05b..4cc105a 100644 --- a/azure-ai-search/package.json +++ b/azure-ai-search/package.json @@ -52,6 +52,7 @@ ], "dependencies": { "@azure/identity": "^4.10.2", + "@azure/search-documents": "^12.1.0", "@modelcontextprotocol/sdk": "^1.12.1", "dotenv": "^16.5.0", "zod": "^3.25.42" diff --git a/azure-ai-search/pnpm-lock.yaml b/azure-ai-search/pnpm-lock.yaml index 407797d..a8422a1 100644 --- a/azure-ai-search/pnpm-lock.yaml +++ b/azure-ai-search/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@azure/identity': specifier: ^4.10.2 version: 4.10.2 + '@azure/search-documents': + specifier: ^12.1.0 + version: 12.1.0 '@modelcontextprotocol/sdk': specifier: ^1.12.1 version: 1.16.0 @@ -63,6 +66,14 @@ packages: resolution: {integrity: sha512-O4aP3CLFNodg8eTHXECaH3B3CjicfzkxVtnrfLkOq0XNP7TIECGfHpK/C6vADZkWP75wzmdBnsIA8ksuJMk18g==} engines: {node: '>=20.0.0'} + '@azure/core-http-compat@2.3.0': + resolution: {integrity: sha512-qLQujmUypBBG0gxHd0j6/Jdmul6ttl24c8WGiLXIk7IHXdBlfoBqW27hyz3Xn6xbfdyVSarl1Ttbk0AwnZBYCw==} + engines: {node: '>=18.0.0'} + + '@azure/core-paging@1.6.2': + resolution: {integrity: sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==} + engines: {node: '>=18.0.0'} + '@azure/core-rest-pipeline@1.22.0': resolution: {integrity: sha512-OKHmb3/Kpm06HypvB3g6Q3zJuvyXcpxDpCS1PnU8OV6AJgSFaee/covXBcPbWc6XDDxtEPlbi3EMQ6nUiPaQtw==} engines: {node: '>=20.0.0'} @@ -95,6 +106,10 @@ packages: resolution: {integrity: sha512-jMeut9UQugcmq7aPWWlJKhJIse4DQ594zc/JaP6BIxg55XaX3aM/jcPuIQ4ryHnI4QSf03wUspy/uqAvjWKbOg==} engines: {node: '>=16'} + '@azure/search-documents@12.1.0': + resolution: {integrity: sha512-IzD+hfqGqFtXymHXm4RzrZW2MsSH2M7RLmZsKaKVi7SUxbeYTUeX+ALk8gVzkM8ykb7EzlDLWCNErKfAa57rYQ==} + engines: {node: '>=18.0.0'} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -436,6 +451,10 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + eventsource-parser@3.0.3: resolution: {integrity: sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==} engines: {node: '>=20.0.0'} @@ -877,6 +896,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@azure/core-http-compat@2.3.0': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-client': 1.10.0 + '@azure/core-rest-pipeline': 1.22.0 + transitivePeerDependencies: + - supports-color + + '@azure/core-paging@1.6.2': + dependencies: + tslib: 2.8.1 + '@azure/core-rest-pipeline@1.22.0': dependencies: '@azure/abort-controller': 2.1.2 @@ -936,6 +967,21 @@ snapshots: jsonwebtoken: 9.0.2 uuid: 8.3.2 + '@azure/search-documents@12.1.0': + dependencies: + '@azure/core-auth': 1.10.0 + '@azure/core-client': 1.10.0 + '@azure/core-http-compat': 2.3.0 + '@azure/core-paging': 1.6.2 + '@azure/core-rest-pipeline': 1.22.0 + '@azure/core-tracing': 1.3.0 + '@azure/core-util': 1.13.0 + '@azure/logger': 1.3.0 + events: 3.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -1223,6 +1269,8 @@ snapshots: etag@1.8.1: {} + events@3.3.0: {} + eventsource-parser@3.0.3: {} eventsource@3.0.7: diff --git a/azure-ai-search/quick-test.mjs b/azure-ai-search/quick-test.mjs new file mode 100644 index 0000000..c2c8cdf --- /dev/null +++ b/azure-ai-search/quick-test.mjs @@ -0,0 +1,64 @@ +// Test simple avec MCP Client +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { spawn } from 'child_process'; + +console.log("🚀 Test rapide Phase 4 - Elite Dangerous"); + +const serverProcess = spawn('node', ['dist/server.js'], { + stdio: ['pipe', 'pipe', 'inherit'], +}); + +const transport = new StdioClientTransport({ + process: serverProcess, +}); + +const client = new Client( + { name: 'test-client', version: '1.0.0' }, + { capabilities: {} } +); + +try { + await client.connect(transport); + console.log("✅ Connecté au serveur MCP"); + + // Test 1: Search classique + console.log("\n📖 Test search-documents..."); + const searchResult = await client.request( + { method: 'tools/call' }, + { + name: 'search-documents', + arguments: { + indexName: 'rag-1753386801239', + searchText: 'thargoid', + top: 2 + } + } + ); + console.log("Résultat search:", searchResult.content[0].text.substring(0, 200) + "..."); + + // Test 2: Semantic search + console.log("\n🧠 Test semantic-search..."); + const semanticResult = await client.request( + { method: 'tools/call' }, + { + name: 'semantic-search', + arguments: { + indexName: 'rag-1753386801239', + searchText: 'Comment combattre les Thargoids ?', + semanticConfiguration: 'rag-1753386801239-semantic-configuration', + answers: { count: 2, threshold: 0.6 }, + top: 3 + } + } + ); + console.log("Résultat semantic:", semanticResult.content[0].text.substring(0, 200) + "..."); + + console.log("\n✅ Tests terminés avec succès !"); + +} catch (error) { + console.error("❌ Erreur:", error.message); +} finally { + await client.close(); + serverProcess.kill(); +} \ No newline at end of file diff --git a/azure-ai-search/resources/index-resources.ts b/azure-ai-search/resources/index-resources.ts new file mode 100644 index 0000000..99dced8 --- /dev/null +++ b/azure-ai-search/resources/index-resources.ts @@ -0,0 +1,103 @@ +import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { getAzureSearchTools } from "../tools/search-tools.js"; + +export class AzureSearchIndexResources { + constructor( + private server: McpServer, + private searchTools: ReturnType + ) {} + + async registerDynamicResources() { + try { + // Get list of available indexes + const indexesResult = await this.searchTools.listIndexes(); + + if (!indexesResult.success || !indexesResult.data) { + console.error("Failed to fetch indexes for resource registration:", indexesResult.error); + return; + } + + const indexes = indexesResult.data; + console.error(`Registering resources for ${indexes.length} indexes`); + + // Register a resource for the indexes list + this.server.resource( + "indexes-list", + new ResourceTemplate("azure-search://indexes", { list: undefined }), + async () => ({ + contents: [ + { + type: "text", + text: JSON.stringify(indexes, null, 2), + }, + ], + }) + ); + + // Register individual index resources + for (const index of indexes) { + const indexName = index.name; + + // Resource for index schema + this.server.resource( + `index-schema-${indexName}`, + new ResourceTemplate(`azure-search://index/${indexName}/schema`, { list: undefined }), + async () => { + const schemaResult = await this.searchTools.getIndex(indexName); + return { + contents: [ + { + type: "text", + text: JSON.stringify(schemaResult, null, 2), + }, + ], + }; + } + ); + + // Resource for index statistics + this.server.resource( + `index-stats-${indexName}`, + new ResourceTemplate(`azure-search://index/${indexName}/statistics`, { list: undefined }), + async () => { + const statsResult = await this.searchTools.getIndexStatistics(indexName); + return { + contents: [ + { + type: "text", + text: JSON.stringify(statsResult, null, 2), + }, + ], + }; + } + ); + + // Resource for sample documents from the index + this.server.resource( + `index-sample-${indexName}`, + new ResourceTemplate(`azure-search://index/${indexName}/sample`, { list: undefined }), + async () => { + const searchResult = await this.searchTools.searchDocuments({ + indexName, + searchText: "*", + top: 5, // Get first 5 documents as sample + }); + return { + contents: [ + { + type: "text", + text: JSON.stringify(searchResult, null, 2), + }, + ], + }; + } + ); + } + + console.error(`Successfully registered ${indexes.length * 3 + 1} resources`); + + } catch (error) { + console.error("Error registering dynamic resources:", error); + } + } +} \ No newline at end of file diff --git a/azure-ai-search/rollup.config.js b/azure-ai-search/rollup.config.js index a7f1ccf..49a0ad6 100644 --- a/azure-ai-search/rollup.config.js +++ b/azure-ai-search/rollup.config.js @@ -7,8 +7,9 @@ import shebang from 'rollup-plugin-preserve-shebang'; export default { input: 'server.ts', output: { - file: 'dist/server.js', - format: 'esm' + dir: 'dist', + format: 'esm', + entryFileNames: 'server.js' }, plugins: [ shebang(), // <- pour le #!/usr/bin/env node @@ -19,6 +20,8 @@ export default { ], external: [ "@modelcontextprotocol/sdk", + "@azure/search-documents", + "@azure/identity", "zod", "dotenv", ] diff --git a/azure-ai-search/server.ts b/azure-ai-search/server.ts index 931f3fc..c73dba6 100644 --- a/azure-ai-search/server.ts +++ b/azure-ai-search/server.ts @@ -1,8 +1,45 @@ #!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { z } from "zod"; import dotenv from "dotenv"; +import { + SearchDocumentsSchema, + GetDocumentSchema, + SuggestSchema, + AutocompleteSchema, + ListIndexesSchema, + GetIndexSchema, + GetIndexStatisticsSchema, + UploadDocumentsSchema, + MergeDocumentsSchema, + DeleteDocumentsSchema, + VectorSearchSchema, + HybridSearchSchema, + SemanticSearchSchema, +} from "./types.js"; +import { + searchDocuments, + getDocument, + suggest, + autocomplete, +} from "./tools/search-tools.js"; +import { + listIndexes, + getIndexSchema, + getIndexStatistics, +} from "./tools/index-tools.js"; +import { + uploadDocuments, + mergeDocuments, + deleteDocuments, +} from "./tools/document-tools.js"; +import { + vectorSearch, + hybridSearch, + semanticSearch, +} from "./tools/vector-tools.js"; +import { getAzureSearchTools } from "./tools/search-tools.js"; +import { AzureSearchIndexResources } from "./resources/index-resources.js"; dotenv.config(); @@ -13,17 +50,424 @@ const server = new McpServer({ description: "MCP server for interacting with Azure AI Search" }); +// Register search tools +server.tool( + "search-documents", + "Search documents in an Azure AI Search index with comprehensive filtering and faceting options", + SearchDocumentsSchema.shape, + async (params) => { + try { + const result = await searchDocuments(params); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + isError: !result.success, + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error searching documents: ${error instanceof Error ? error.message : "Unknown error"}`, + }, + ], + isError: true, + }; + } + } +); + +server.tool( + "get-document", + "Retrieve a specific document by its key from an Azure AI Search index", + GetDocumentSchema.shape, + async (params) => { + try { + const result = await getDocument(params); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + isError: !result.success, + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error retrieving document: ${error instanceof Error ? error.message : "Unknown error"}`, + }, + ], + isError: true, + }; + } + } +); + +server.tool( + "suggest", + "Get search suggestions using a configured suggester in Azure AI Search", + SuggestSchema.shape, + async (params) => { + try { + const result = await suggest(params); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + isError: !result.success, + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error getting suggestions: ${error instanceof Error ? error.message : "Unknown error"}`, + }, + ], + isError: true, + }; + } + } +); + +server.tool( + "autocomplete", + "Get autocomplete suggestions for partial search terms using Azure AI Search", + AutocompleteSchema.shape, + async (params) => { + try { + const result = await autocomplete(params); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + isError: !result.success, + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error getting autocomplete: ${error instanceof Error ? error.message : "Unknown error"}`, + }, + ], + isError: true, + }; + } + } +); + +// Register index management tools +server.tool( + "list-indexes", + "List all available search indexes in the Azure AI Search service", + ListIndexesSchema.shape, + async (params) => { + try { + const result = await listIndexes(params); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + isError: !result.success, + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error listing indexes: ${error instanceof Error ? error.message : "Unknown error"}`, + }, + ], + isError: true, + }; + } + } +); + +server.tool( + "get-index-schema", + "Get the schema definition of a specific search index including fields, analyzers, and configuration", + GetIndexSchema.shape, + async (params) => { + try { + const result = await getIndexSchema(params); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + isError: !result.success, + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error getting index schema: ${error instanceof Error ? error.message : "Unknown error"}`, + }, + ], + isError: true, + }; + } + } +); + +server.tool( + "get-index-statistics", + "Get statistics and usage information for a specific search index", + GetIndexStatisticsSchema.shape, + async (params) => { + try { + const result = await getIndexStatistics(params); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + isError: !result.success, + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error getting index statistics: ${error instanceof Error ? error.message : "Unknown error"}`, + }, + ], + isError: true, + }; + } + } +); + +// Register document management tools +server.tool( + "upload-documents", + "Upload new documents to an Azure AI Search index. Documents will be added or updated if they already exist.", + UploadDocumentsSchema.shape, + async (params) => { + try { + const result = await uploadDocuments(params); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + isError: !result.success, + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error uploading documents: ${error instanceof Error ? error.message : "Unknown error"}`, + }, + ], + isError: true, + }; + } + } +); + +server.tool( + "merge-documents", + "Merge or update existing documents in an Azure AI Search index. Only provided fields will be updated.", + MergeDocumentsSchema.shape, + async (params) => { + try { + const result = await mergeDocuments(params); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + isError: !result.success, + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error merging documents: ${error instanceof Error ? error.message : "Unknown error"}`, + }, + ], + isError: true, + }; + } + } +); + +server.tool( + "delete-documents", + "Delete documents from an Azure AI Search index by their key values.", + DeleteDocumentsSchema.shape, + async (params) => { + try { + const result = await deleteDocuments(params); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + isError: !result.success, + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error deleting documents: ${error instanceof Error ? error.message : "Unknown error"}`, + }, + ], + isError: true, + }; + } + } +); + +// Register vector and semantic search tools (Phase 4) +server.tool( + "vector-search", + "Perform pure vector similarity search using k-nearest neighbors on vector fields in Azure AI Search", + VectorSearchSchema.shape, + async (params) => { + try { + const result = await vectorSearch(params); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + isError: !result.success, + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error in vector search: ${error instanceof Error ? error.message : "Unknown error"}`, + }, + ], + isError: true, + }; + } + } +); + +server.tool( + "hybrid-search", + "Perform hybrid search combining traditional text search with vector similarity search for enhanced relevance", + HybridSearchSchema.shape, + async (params) => { + try { + const result = await hybridSearch(params); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + isError: !result.success, + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error in hybrid search: ${error instanceof Error ? error.message : "Unknown error"}`, + }, + ], + isError: true, + }; + } + } +); + +server.tool( + "semantic-search", + "Perform semantic search using Azure AI Search semantic capabilities with answers and captions", + SemanticSearchSchema.shape, + async (params) => { + try { + const result = await semanticSearch(params); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + isError: !result.success, + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error in semantic search: ${error instanceof Error ? error.message : "Unknown error"}`, + }, + ], + isError: true, + }; + } + } +); // Start the server async function main() { - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error("Azure Storage MCP Server running on stdio"); -} + // Register dynamic resources for discovered indexes + const searchTools = getAzureSearchTools(); + const indexResourceManager = new AzureSearchIndexResources(server, searchTools); + await indexResourceManager.registerDynamicResources(); const transport = new StdioServerTransport(); await server.connect(transport); - console.error("Azure AI Searh MCP Server running on stdio"); + console.error("Azure AI Search MCP Server running on stdio"); +} main().catch((error) => { console.error("Fatal error in main():", error); diff --git a/azure-ai-search/test-phase4.js b/azure-ai-search/test-phase4.js new file mode 100644 index 0000000..89a3517 --- /dev/null +++ b/azure-ai-search/test-phase4.js @@ -0,0 +1,82 @@ +#!/usr/bin/env node + +// Test script pour les nouvelles fonctionnalités Phase 4 +// Vector Search et Semantic Search sur l'index Elite Dangerous + +import { searchDocuments } from './dist/tools/search-tools.js'; +import { vectorSearch, semanticSearch } from './dist/tools/vector-tools.js'; + +const indexName = "rag-1753386801239"; + +console.log("🚀 Test Phase 4 - Vector & Semantic Search sur Elite Dangerous RAG"); +console.log("================================================================\n"); + +// Test 1: Recherche classique sur les Thargoids +console.log("📖 Test 1: Recherche classique - 'thargoid combat'"); +try { + const classicResult = await searchDocuments({ + indexName, + searchText: "thargoid combat", + top: 3, + highlightFields: ["chunk", "title"] + }); + + console.log("Résultats:", classicResult.success ? classicResult.data.results.length : "Erreur"); + if (classicResult.success && classicResult.data.results.length > 0) { + console.log("Premier résultat:", classicResult.data.results[0]?.document?.title || "N/A"); + } + console.log("---"); +} catch (error) { + console.log("Erreur test 1:", error.message); +} + +// Test 2: Recherche vectorielle pure (nécessite un vrai embedding) +console.log("🔍 Test 2: Vector Search - simulation"); +try { + // Vector simulé - en vrai il faudrait un embedding d'OpenAI pour "thargoid combat" + const dummyVector = Array(1536).fill(0).map(() => Math.random() * 0.1); + + const vectorResult = await vectorSearch({ + indexName, + vectorQueries: [{ + vector: dummyVector, + fields: "text_vector", + k: 3 + }], + top: 3 + }); + + console.log("Résultats vector:", vectorResult.success ? "OK" : vectorResult.error); +} catch (error) { + console.log("Erreur test 2:", error.message); +} + +// Test 3: Recherche sémantique avec réponses +console.log("🧠 Test 3: Semantic Search avec réponses"); +try { + const semanticResult = await semanticSearch({ + indexName, + searchText: "Comment combattre efficacement les Thargoids dans Elite Dangerous ?", + semanticConfiguration: "rag-1753386801239-semantic-configuration", + answers: { + answerType: "extractive", + count: 2, + threshold: 0.6 + }, + captions: { + captionType: "extractive", + highlight: true + }, + top: 5 + }); + + console.log("Résultats semantic:", semanticResult.success ? "OK" : semanticResult.error); + if (semanticResult.success) { + console.log("Réponses trouvées:", semanticResult.data?.answers?.length || 0); + console.log("Captions trouvées:", semanticResult.data?.captions?.length || 0); + } +} catch (error) { + console.log("Erreur test 3:", error.message); +} + +console.log("\n✅ Tests Phase 4 terminés !"); \ No newline at end of file diff --git a/azure-ai-search/tools/document-tools.ts b/azure-ai-search/tools/document-tools.ts new file mode 100644 index 0000000..fd70fbd --- /dev/null +++ b/azure-ai-search/tools/document-tools.ts @@ -0,0 +1,109 @@ +import { + UploadDocumentsSchema, + MergeDocumentsSchema, + DeleteDocumentsSchema, +} from "../types.js"; + +// Reuse the same singleton instance +import { getAzureSearchTools } from "./search-tools.js"; + +export async function uploadDocuments(params: any) { + try { + console.error("=== DEBUG uploadDocuments tool: params received ===", { + indexName: params.indexName, + documentsCount: params.documents?.length, + firstDocKeys: params.documents?.[0] ? Object.keys(params.documents[0]) : [] + }); + + const validatedParams = UploadDocumentsSchema.parse(params); + console.error("=== DEBUG uploadDocuments tool: validated params ===", { + indexName: validatedParams.indexName, + documentsCount: validatedParams.documents.length + }); + + const tools = getAzureSearchTools(); + const result = await tools.uploadDocuments(validatedParams as any); + + console.error("=== DEBUG uploadDocuments tool: result ===", { + success: result.success, + resultsCount: result.data?.results?.length, + error: result.error + }); + + return result; + } catch (error) { + console.error("=== DEBUG uploadDocuments tool: error ===", error); + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error in uploadDocuments tool" + }; + } +} + +export async function mergeDocuments(params: any) { + try { + console.error("=== DEBUG mergeDocuments tool: params received ===", { + indexName: params.indexName, + documentsCount: params.documents?.length, + firstDocKeys: params.documents?.[0] ? Object.keys(params.documents[0]) : [] + }); + + const validatedParams = MergeDocumentsSchema.parse(params); + console.error("=== DEBUG mergeDocuments tool: validated params ===", { + indexName: validatedParams.indexName, + documentsCount: validatedParams.documents.length + }); + + const tools = getAzureSearchTools(); + const result = await tools.mergeDocuments(validatedParams as any); + + console.error("=== DEBUG mergeDocuments tool: result ===", { + success: result.success, + resultsCount: result.data?.results?.length, + error: result.error + }); + + return result; + } catch (error) { + console.error("=== DEBUG mergeDocuments tool: error ===", error); + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error in mergeDocuments tool" + }; + } +} + +export async function deleteDocuments(params: any) { + try { + console.error("=== DEBUG deleteDocuments tool: params received ===", { + indexName: params.indexName, + keyField: params.keyField, + keyValuesCount: params.keyValues?.length, + keyValues: params.keyValues + }); + + const validatedParams = DeleteDocumentsSchema.parse(params); + console.error("=== DEBUG deleteDocuments tool: validated params ===", { + indexName: validatedParams.indexName, + keyField: validatedParams.keyField, + keyValuesCount: validatedParams.keyValues.length + }); + + const tools = getAzureSearchTools(); + const result = await tools.deleteDocuments(validatedParams as any); + + console.error("=== DEBUG deleteDocuments tool: result ===", { + success: result.success, + resultsCount: result.data?.results?.length, + error: result.error + }); + + return result; + } catch (error) { + console.error("=== DEBUG deleteDocuments tool: error ===", error); + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error in deleteDocuments tool" + }; + } +} \ No newline at end of file diff --git a/azure-ai-search/tools/index-tools.ts b/azure-ai-search/tools/index-tools.ts new file mode 100644 index 0000000..2c84677 --- /dev/null +++ b/azure-ai-search/tools/index-tools.ts @@ -0,0 +1,47 @@ +import { + ListIndexesSchema, + GetIndexSchema, + GetIndexStatisticsSchema, +} from "../types.js"; + +// Reuse the same singleton instance +import { getAzureSearchTools } from "./search-tools.js"; + +export async function listIndexes(params: any) { + try { + console.error("=== DEBUG listIndexes tool: params received ===", params); + + // Handle empty params case + const validatedParams = params ? ListIndexesSchema.parse(params) : {}; + console.error("=== DEBUG listIndexes tool: validated params ===", validatedParams); + + const tools = getAzureSearchTools(); + const result = await tools.listIndexes(validatedParams as any); + + console.error("=== DEBUG listIndexes tool: result ===", { + success: result.success, + dataLength: result.data?.length, + error: result.error + }); + + return result; + } catch (error) { + console.error("=== DEBUG listIndexes tool: error ===", error); + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error in listIndexes tool" + }; + } +} + +export async function getIndexSchema(params: any) { + const validatedParams = GetIndexSchema.parse(params); + const tools = getAzureSearchTools(); + return await tools.getIndex(validatedParams.indexName); +} + +export async function getIndexStatistics(params: any) { + const validatedParams = GetIndexStatisticsSchema.parse(params); + const tools = getAzureSearchTools(); + return await tools.getIndexStatistics(validatedParams.indexName); +} \ No newline at end of file diff --git a/azure-ai-search/tools/search-tools.ts b/azure-ai-search/tools/search-tools.ts new file mode 100644 index 0000000..8934b3a --- /dev/null +++ b/azure-ai-search/tools/search-tools.ts @@ -0,0 +1,41 @@ +import { AzureSearchTools } from "../lib/azure-search-client.js"; +import { + SearchDocumentsSchema, + GetDocumentSchema, + SuggestSchema, + AutocompleteSchema, +} from "../types.js"; + +// Singleton instance with lazy loading +let azureSearchTools: AzureSearchTools | null = null; + +export function getAzureSearchTools(): AzureSearchTools { + if (!azureSearchTools) { + azureSearchTools = new AzureSearchTools(); + } + return azureSearchTools; +} + +export async function searchDocuments(params: any) { + const validatedParams = SearchDocumentsSchema.parse(params); + const tools = getAzureSearchTools(); + return await tools.searchDocuments(validatedParams as any); +} + +export async function getDocument(params: any) { + const validatedParams = GetDocumentSchema.parse(params); + const tools = getAzureSearchTools(); + return await tools.getDocument(validatedParams as any); +} + +export async function suggest(params: any) { + const validatedParams = SuggestSchema.parse(params); + const tools = getAzureSearchTools(); + return await tools.suggest(validatedParams as any); +} + +export async function autocomplete(params: any) { + const validatedParams = AutocompleteSchema.parse(params); + const tools = getAzureSearchTools(); + return await tools.autocomplete(validatedParams as any); +} \ No newline at end of file diff --git a/azure-ai-search/tools/vector-tools.ts b/azure-ai-search/tools/vector-tools.ts new file mode 100644 index 0000000..9925948 --- /dev/null +++ b/azure-ai-search/tools/vector-tools.ts @@ -0,0 +1,50 @@ +import { + VectorSearchSchema, + HybridSearchSchema, + SemanticSearchSchema, +} from "../types.js"; + +// Reuse the same singleton instance +import { getAzureSearchTools } from "./search-tools.js"; + +export async function vectorSearch(params: any) { + try { + const validatedParams = VectorSearchSchema.parse(params); + const tools = getAzureSearchTools(); + const result = await tools.vectorSearch(validatedParams as any); + return result; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error in vectorSearch tool" + }; + } +} + +export async function hybridSearch(params: any) { + try { + const validatedParams = HybridSearchSchema.parse(params); + const tools = getAzureSearchTools(); + const result = await tools.hybridSearch(validatedParams as any); + return result; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error in hybridSearch tool" + }; + } +} + +export async function semanticSearch(params: any) { + try { + const validatedParams = SemanticSearchSchema.parse(params); + const tools = getAzureSearchTools(); + const result = await tools.semanticSearch(validatedParams as any); + return result; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error in semanticSearch tool" + }; + } +} \ No newline at end of file diff --git a/azure-ai-search/types.ts b/azure-ai-search/types.ts index ed30420..a71d8b0 100644 --- a/azure-ai-search/types.ts +++ b/azure-ai-search/types.ts @@ -1,521 +1,270 @@ import { z } from "zod"; -// Validation pour les noms de table Azure -const azureTableNameRegex = /^[a-zA-Z][a-zA-Z0-9]{2,62}$/; -const azureKeyRegex = /^[^/\\#?]*$/; - -// Schema pour les paramètres de connexion Azure -export const AzureTableConfigSchema = z.object({ - accountName: z.string(), - accountKey: z.string().optional(), - tableName: z.string(), - connectionString: z.string().optional(), +// Azure AI Search Configuration +export const AzureSearchConfigSchema = z.object({ + endpoint: z.string().url().describe("Azure Search service endpoint"), + apiKey: z.string().optional().describe("Azure Search API key"), + apiVersion: z.string().optional().default("2023-11-01").describe("Azure Search API version"), }); -export type AzureTableConfig = z.infer; +export type AzureSearchConfig = z.infer; -// Schema pour les filtres de requête -export const TableQuerySchema = z.object({ - filter: z.string().optional(), - select: z.array(z.string()).optional(), - maxResults: z.number().optional(), +// Search Query Parameters +export const SearchDocumentsSchema = z.object({ + indexName: z.string().min(1).describe("Name of the search index"), + searchText: z.string().describe("Text to search for (use '*' for all documents)"), + searchMode: z.enum(["any", "all"]).optional().default("any").describe("Search mode: any (OR) or all (AND)"), + searchFields: z.array(z.string()).optional().describe("Fields to search in"), + select: z.array(z.string()).optional().describe("Fields to include in results"), + filter: z.string().optional().describe("OData filter expression"), + orderBy: z.array(z.string()).optional().describe("Sort order (field asc/desc)"), + top: z.number().min(1).max(1000).optional().default(50).describe("Number of results to return (1-1000)"), + skip: z.number().min(0).optional().describe("Number of results to skip"), + includeTotalCount: z.boolean().optional().default(false).describe("Include total count in response"), + facets: z.array(z.string()).optional().describe("Facet fields"), + highlightFields: z.array(z.string()).optional().describe("Fields to highlight"), + highlightPreTag: z.string().optional().default("").describe("Pre-tag for highlighting"), + highlightPostTag: z.string().optional().default("").describe("Post-tag for highlighting"), + minimumCoverage: z.number().min(0).max(100).optional().describe("Minimum coverage percentage"), + queryType: z.enum(["simple", "full"]).optional().default("simple").describe("Query type: simple or full (Lucene)"), }); -export type TableQuery = z.infer; +export type SearchDocumentsParams = z.infer; -// Schema pour les entités de table Azure -export const TableEntitySchema = z.record(z.any()); - -export type TableEntity = z.infer; - -// Schema pour les paramètres d'outil MCP -export const ReadTableToolSchema = z.object({ - tableName: z.string().describe("Nom de la table Azure à lire"), - filter: z.string().optional().describe("Filtre OData pour les entités (ex: 'PartitionKey eq \"partition1\"')"), - select: z.array(z.string()).optional().describe("Colonnes à sélectionner"), - maxResults: z.number().optional().describe("Nombre maximum d'entités à retourner"), -}); - -// Schema pour les requêtes avancées avec pagination -export const QueryTableAdvancedSchema = z.object({ - tableName: z.string() - .regex(azureTableNameRegex, "Le nom de table doit commencer par une lettre et contenir seulement des lettres/chiffres (3-63 caractères)") - .describe("Nom de la table Azure à interroger"), - filter: z.string().optional().describe("Filtre OData complexe (ex: 'Age gt 18 and Status eq \"active\"')"), - select: z.array(z.string()).optional().describe("Colonnes à sélectionner"), - orderBy: z.array(z.string()).optional().describe("Tri par colonnes (ex: ['Age desc', 'Name asc'])"), - top: z.number().min(1).max(1000).optional().describe("Nombre d'entités à retourner (1-1000)"), - skip: z.number().min(0).optional().describe("Nombre d'entités à ignorer"), - continuationToken: z.string().optional().describe("Token de continuation pour la pagination"), -}); - -export type QueryTableAdvancedParams = z.infer; - -export type ReadTableToolParams = z.infer; - -// Schema pour les valeurs autorisées dans Azure Table -const AzureTableValueSchema = z.union([ - z.string().max(64000), // String max 64KB - z.number().int().min(-2147483648).max(2147483647), // Int32 - z.number().min(-1.79E+308).max(1.79E+308), // Double - z.boolean(), - z.date(), - z.instanceof(Uint8Array).refine(data => data.length <= 64000, "Binary data must be <= 64KB"), - z.null() -]); - -// Schema pour créer une entité -export const CreateEntityToolSchema = z.object({ - tableName: z.string() - .regex(azureTableNameRegex, "Le nom de table doit commencer par une lettre et contenir seulement des lettres/chiffres (3-63 caractères)") - .describe("Nom de la table Azure"), - partitionKey: z.string() - .min(1, "PartitionKey ne peut pas être vide") - .max(1024, "PartitionKey ne peut pas dépasser 1024 caractères") - .regex(azureKeyRegex, "PartitionKey ne peut pas contenir les caractères /, \\, #, ?") - .describe("Clé de partition de l'entité"), - rowKey: z.string() - .min(1, "RowKey ne peut pas être vide") - .max(1024, "RowKey ne peut pas dépasser 1024 caractères") - .regex(azureKeyRegex, "RowKey ne peut pas contenir les caractères /, \\, #, ?") - .describe("Clé de ligne de l'entité"), - entity: z.record(AzureTableValueSchema) - .refine(data => { - // Vérifier qu'il n'y a pas de propriétés réservées - const reservedProps = ['PartitionKey', 'RowKey', 'Timestamp', 'ETag']; - const hasReserved = Object.keys(data).some(key => reservedProps.includes(key)); - return !hasReserved; - }, "L'entité ne peut pas contenir les propriétés réservées : PartitionKey, RowKey, Timestamp, ETag") - .refine(data => Object.keys(data).length <= 252, "Une entité ne peut pas avoir plus de 252 propriétés") - .describe("Données de l'entité à créer (max 252 propriétés, types autorisés : string, number, boolean, date, binary, null)"), -}); - -export type CreateEntityToolParams = z.infer; - -// Schema pour mettre à jour une entité -export const UpdateEntityToolSchema = z.object({ - tableName: z.string() - .regex(azureTableNameRegex, "Le nom de table doit commencer par une lettre et contenir seulement des lettres/chiffres (3-63 caractères)") - .describe("Nom de la table Azure"), - partitionKey: z.string() - .min(1, "PartitionKey ne peut pas être vide") - .max(1024, "PartitionKey ne peut pas dépasser 1024 caractères") - .regex(azureKeyRegex, "PartitionKey ne peut pas contenir les caractères /, \\, #, ?") - .describe("Clé de partition de l'entité"), - rowKey: z.string() - .min(1, "RowKey ne peut pas être vide") - .max(1024, "RowKey ne peut pas dépasser 1024 caractères") - .regex(azureKeyRegex, "RowKey ne peut pas contenir les caractères /, \\, #, ?") - .describe("Clé de ligne de l'entité"), - entity: z.record(AzureTableValueSchema) - .refine(data => { - // Vérifier qu'il n'y a pas de propriétés réservées - const reservedProps = ['PartitionKey', 'RowKey', 'Timestamp', 'ETag']; - const hasReserved = Object.keys(data).some(key => reservedProps.includes(key)); - return !hasReserved; - }, "L'entité ne peut pas contenir les propriétés réservées : PartitionKey, RowKey, Timestamp, ETag") - .refine(data => Object.keys(data).length <= 252, "Une entité ne peut pas avoir plus de 252 propriétés") - .describe("Données de l'entité à mettre à jour (max 252 propriétés, types autorisés : string, number, boolean, date, binary, null)"), - mode: z.enum(["merge", "replace"]).optional().default("merge").describe("Mode de mise à jour : merge ou replace"), -}); - -export type UpdateEntityToolParams = z.infer; - -// Schema pour supprimer une entité -export const DeleteEntityToolSchema = z.object({ - tableName: z.string() - .regex(azureTableNameRegex, "Le nom de table doit commencer par une lettre et contenir seulement des lettres/chiffres (3-63 caractères)") - .describe("Nom de la table Azure"), - partitionKey: z.string() - .min(1, "PartitionKey ne peut pas être vide") - .max(1024, "PartitionKey ne peut pas dépasser 1024 caractères") - .regex(azureKeyRegex, "PartitionKey ne peut pas contenir les caractères /, \\, #, ?") - .describe("Clé de partition de l'entité"), - rowKey: z.string() - .min(1, "RowKey ne peut pas être vide") - .max(1024, "RowKey ne peut pas dépasser 1024 caractères") - .regex(azureKeyRegex, "RowKey ne peut pas contenir les caractères /, \\, #, ?") - .describe("Clé de ligne de l'entité"), -}); - -export type DeleteEntityToolParams = z.infer; - -// Schema pour les opérations batch -export const BatchCreateEntitiesSchema = z.object({ - tableName: z.string() - .regex(azureTableNameRegex, "Le nom de table doit commencer par une lettre et contenir seulement des lettres/chiffres (3-63 caractères)") - .describe("Nom de la table Azure"), - entities: z.array(z.object({ - partitionKey: z.string() - .min(1, "PartitionKey ne peut pas être vide") - .max(1024, "PartitionKey ne peut pas dépasser 1024 caractères") - .regex(azureKeyRegex, "PartitionKey ne peut pas contenir les caractères /, \\, #, ?"), - rowKey: z.string() - .min(1, "RowKey ne peut pas être vide") - .max(1024, "RowKey ne peut pas dépasser 1024 caractères") - .regex(azureKeyRegex, "RowKey ne peut pas contenir les caractères /, \\, #, ?"), - entity: z.record(AzureTableValueSchema) - .refine(data => { - const reservedProps = ['PartitionKey', 'RowKey', 'Timestamp', 'ETag']; - const hasReserved = Object.keys(data).some(key => reservedProps.includes(key)); - return !hasReserved; - }, "L'entité ne peut pas contenir les propriétés réservées") - .refine(data => Object.keys(data).length <= 252, "Une entité ne peut pas avoir plus de 252 propriétés") - })) - .min(1, "Au moins une entité est requise") - .max(100, "Maximum 100 entités par batch (limitation Azure)") - .describe("Liste des entités à créer"), -}); - -export type BatchCreateEntitiesParams = z.infer; - -export const BatchUpdateEntitiesSchema = z.object({ - tableName: z.string() - .regex(azureTableNameRegex, "Le nom de table doit commencer par une lettre et contenir seulement des lettres/chiffres (3-63 caractères)") - .describe("Nom de la table Azure"), - entities: z.array(z.object({ - partitionKey: z.string() - .min(1, "PartitionKey ne peut pas être vide") - .max(1024, "PartitionKey ne peut pas dépasser 1024 caractères") - .regex(azureKeyRegex, "PartitionKey ne peut pas contenir les caractères /, \\, #, ?"), - rowKey: z.string() - .min(1, "RowKey ne peut pas être vide") - .max(1024, "RowKey ne peut pas dépasser 1024 caractères") - .regex(azureKeyRegex, "RowKey ne peut pas contenir les caractères /, \\, #, ?"), - entity: z.record(AzureTableValueSchema) - .refine(data => { - const reservedProps = ['PartitionKey', 'RowKey', 'Timestamp', 'ETag']; - const hasReserved = Object.keys(data).some(key => reservedProps.includes(key)); - return !hasReserved; - }, "L'entité ne peut pas contenir les propriétés réservées") - .refine(data => Object.keys(data).length <= 252, "Une entité ne peut pas avoir plus de 252 propriétés"), - mode: z.enum(["merge", "replace"]).optional().default("merge") - })) - .min(1, "Au moins une entité est requise") - .max(100, "Maximum 100 entités par batch (limitation Azure)") - .describe("Liste des entités à mettre à jour"), -}); - -export type BatchUpdateEntitiesParams = z.infer; - -export const BatchDeleteEntitiesSchema = z.object({ - tableName: z.string() - .regex(azureTableNameRegex, "Le nom de table doit commencer par une lettre et contenir seulement des lettres/chiffres (3-63 caractères)") - .describe("Nom de la table Azure"), - entities: z.array(z.object({ - partitionKey: z.string() - .min(1, "PartitionKey ne peut pas être vide") - .max(1024, "PartitionKey ne peut pas dépasser 1024 caractères") - .regex(azureKeyRegex, "PartitionKey ne peut pas contenir les caractères /, \\, #, ?"), - rowKey: z.string() - .min(1, "RowKey ne peut pas être vide") - .max(1024, "RowKey ne peut pas dépasser 1024 caractères") - .regex(azureKeyRegex, "RowKey ne peut pas contenir les caractères /, \\, #, ?") - })) - .min(1, "Au moins une entité est requise") - .max(100, "Maximum 100 entités par batch (limitation Azure)") - .describe("Liste des entités à supprimer"), -}); - -export type BatchDeleteEntitiesParams = z.infer; - -// Schema pour la gestion des tables -export const CreateTableSchema = z.object({ - tableName: z.string() - .regex(azureTableNameRegex, "Le nom de table doit commencer par une lettre et contenir seulement des lettres/chiffres (3-63 caractères)") - .describe("Nom de la table Azure à créer"), -}); - -export type CreateTableParams = z.infer; - -export const DeleteTableSchema = z.object({ - tableName: z.string() - .regex(azureTableNameRegex, "Le nom de table doit commencer par une lettre et contenir seulement des lettres/chiffres (3-63 caractères)") - .describe("Nom de la table Azure à supprimer"), -}); - -export type DeleteTableParams = z.infer; - -// Azure Blob Storage schemas -const azureContainerNameRegex = /^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])?$/; -const azureBlobNameRegex = /^[^\\/]+$/; - -// Schema pour les paramètres de connexion Azure Blob -export const AzureBlobConfigSchema = z.object({ - accountName: z.string().optional(), - connectionString: z.string().optional(), -}).refine(data => data.accountName || data.connectionString, { - message: "Either accountName or connectionString must be provided" -}); - -export type AzureBlobConfig = z.infer; - -// Schema pour créer un container -export const CreateContainerSchema = z.object({ - containerName: z.string() - .min(3, "Le nom du container doit contenir au moins 3 caractères") - .max(63, "Le nom du container ne peut pas dépasser 63 caractères") - .regex(azureContainerNameRegex, "Le nom du container doit contenir seulement des lettres minuscules, chiffres et tirets") - .describe("Nom du container à créer"), - publicAccess: z.enum(["container", "blob"]).optional().describe("Niveau d'accès public (container ou blob)"), -}); - -export type CreateContainerParams = z.infer; - -// Schema pour supprimer un container -export const DeleteContainerSchema = z.object({ - containerName: z.string() - .regex(azureContainerNameRegex, "Nom de container invalide") - .describe("Nom du container à supprimer"), -}); - -export type DeleteContainerParams = z.infer; - -// Schema pour lister les blobs -export const ListBlobsSchema = z.object({ - containerName: z.string() - .regex(azureContainerNameRegex, "Nom de container invalide") - .describe("Nom du container"), - prefix: z.string().optional().describe("Préfixe pour filtrer les blobs"), -}); - -export type ListBlobsParams = z.infer; - -// Schema pour uploader un blob -export const UploadBlobSchema = z.object({ - containerName: z.string() - .regex(azureContainerNameRegex, "Nom de container invalide") - .describe("Nom du container"), - blobName: z.string() - .min(1, "Le nom du blob ne peut pas être vide") - .max(1024, "Le nom du blob ne peut pas dépasser 1024 caractères") - .describe("Nom du blob à uploader"), - content: z.string().describe("Contenu du blob (texte ou base64)"), - contentType: z.string().optional().describe("Type MIME du contenu"), - metadata: z.record(z.string()).optional().describe("Métadonnées du blob"), - overwrite: z.boolean().optional().default(false).describe("Remplacer le blob s'il existe déjà"), -}); - -export type UploadBlobParams = z.infer; - -// Schema pour télécharger un blob -export const DownloadBlobSchema = z.object({ - containerName: z.string() - .regex(azureContainerNameRegex, "Nom de container invalide") - .describe("Nom du container"), - blobName: z.string() - .min(1, "Le nom du blob ne peut pas être vide") - .describe("Nom du blob à télécharger"), -}); - -export type DownloadBlobParams = z.infer; - -// Schema pour supprimer un blob -export const DeleteBlobSchema = z.object({ - containerName: z.string() - .regex(azureContainerNameRegex, "Nom de container invalide") - .describe("Nom du container"), - blobName: z.string() - .min(1, "Le nom du blob ne peut pas être vide") - .describe("Nom du blob à supprimer"), -}); - -export type DeleteBlobParams = z.infer; - -// Schema pour obtenir les propriétés d'un blob -export const GetBlobPropertiesSchema = z.object({ - containerName: z.string() - .regex(azureContainerNameRegex, "Nom de container invalide") - .describe("Nom du container"), - blobName: z.string() - .min(1, "Le nom du blob ne peut pas être vide") - .describe("Nom du blob"), +// Get Document Parameters +export const GetDocumentSchema = z.object({ + indexName: z.string().min(1).describe("Name of the search index"), + key: z.string().describe("Document key"), + select: z.array(z.string()).optional().describe("Fields to include in result"), }); -export type GetBlobPropertiesParams = z.infer; - -// Azure Service Bus Queue schemas -const azureQueueNameRegex = /^[a-zA-Z0-9]([a-zA-Z0-9\-._]){0,258}[a-zA-Z0-9]$/; - -// Schema pour les paramètres de connexion Azure Service Bus -export const AzureQueueConfigSchema = z.object({ - namespaceName: z.string().optional(), - connectionString: z.string().optional(), -}).refine(data => data.namespaceName || data.connectionString, { - message: "Either namespaceName or connectionString must be provided" -}); - -export type AzureQueueConfig = z.infer; - -// Schema pour créer une queue -export const CreateQueueSchema = z.object({ - queueName: z.string() - .min(1, "Le nom de la queue ne peut pas être vide") - .max(260, "Le nom de la queue ne peut pas dépasser 260 caractères") - .regex(azureQueueNameRegex, "Le nom de la queue doit commencer et finir par une lettre/chiffre, peut contenir lettres, chiffres, tirets, points et underscores") - .describe("Nom de la queue à créer"), - maxSizeInMegabytes: z.number().min(1).max(5120).optional().describe("Taille max en MB (1-5120)"), - defaultMessageTimeToLive: z.string().optional().describe("TTL par défaut des messages (format ISO 8601, ex: P14D pour 14 jours)"), - lockDuration: z.string().optional().describe("Durée de verrouillage des messages (format ISO 8601, ex: PT30S pour 30 secondes)"), - requiresDuplicateDetection: z.boolean().optional().describe("Activer la détection de doublons"), - requiresSession: z.boolean().optional().describe("Requiert des sessions"), - deadLetteringOnMessageExpiration: z.boolean().optional().describe("Activer dead letter sur expiration"), -}); - -export type CreateQueueParams = z.infer; - -// Schema pour supprimer une queue -export const DeleteQueueSchema = z.object({ - queueName: z.string() - .regex(azureQueueNameRegex, "Nom de queue invalide") - .describe("Nom de la queue à supprimer"), -}); - -export type DeleteQueueParams = z.infer; - -// Schema pour envoyer un message -export const SendMessageSchema = z.object({ - queueName: z.string() - .regex(azureQueueNameRegex, "Nom de queue invalide") - .describe("Nom de la queue"), - messageBody: z.string() - .min(1, "Le corps du message ne peut pas être vide") - .describe("Corps du message à envoyer"), - messageId: z.string().optional().describe("ID unique du message"), - correlationId: z.string().optional().describe("ID de corrélation"), - label: z.string().optional().describe("Label/sujet du message"), - timeToLive: z.number().optional().describe("TTL du message en millisecondes"), - sessionId: z.string().optional().describe("ID de session (si sessions activées)"), - userProperties: z.record(z.any()).optional().describe("Propriétés personnalisées du message"), -}); - -export type SendMessageParams = z.infer; - -// Schema pour recevoir des messages -export const ReceiveMessageSchema = z.object({ - queueName: z.string() - .regex(azureQueueNameRegex, "Nom de queue invalide") - .describe("Nom de la queue"), - maxMessageCount: z.number().min(1).max(100).optional().describe("Nombre max de messages à recevoir (1-100)"), - maxWaitTimeInMs: z.number().min(1).max(300000).optional().describe("Temps d'attente max en ms (1-300000)"), -}); - -export type ReceiveMessageParams = z.infer; - -// Schema pour aperçu des messages -export const PeekMessageSchema = z.object({ - queueName: z.string() - .regex(azureQueueNameRegex, "Nom de queue invalide") - .describe("Nom de la queue"), - maxMessageCount: z.number().min(1).max(100).optional().describe("Nombre max de messages à apercevoir (1-100)"), -}); - -export type PeekMessageParams = z.infer; - -// Schema pour obtenir les propriétés d'une queue -export const GetQueuePropertiesSchema = z.object({ - queueName: z.string() - .regex(azureQueueNameRegex, "Nom de queue invalide") - .describe("Nom de la queue"), -}); - -export type GetQueuePropertiesParams = z.infer; - -// Azure Storage Queue schemas (différent des Service Bus Queues) -const azureStorageQueueNameRegex = /^[a-z0-9]([a-z0-9-]){1,61}[a-z0-9]$/; - -// Schema pour les paramètres de connexion Azure Storage Queue -export const AzureStorageQueueConfigSchema = z.object({ - accountName: z.string().optional(), - connectionString: z.string().optional(), -}).refine(data => data.accountName || data.connectionString, { - message: "Either accountName or connectionString must be provided" -}); - -export type AzureStorageQueueConfig = z.infer; - -// Schema pour créer une queue storage -export const CreateStorageQueueSchema = z.object({ - queueName: z.string() - .min(3, "Le nom de la queue doit contenir au moins 3 caractères") - .max(63, "Le nom de la queue ne peut pas dépasser 63 caractères") - .regex(azureStorageQueueNameRegex, "Le nom de la queue doit contenir seulement des lettres minuscules, chiffres et tirets") - .describe("Nom de la queue à créer"), - metadata: z.record(z.string()).optional().describe("Métadonnées de la queue"), -}); - -export type CreateStorageQueueParams = z.infer; - -// Schema pour supprimer une queue storage -export const DeleteStorageQueueSchema = z.object({ - queueName: z.string() - .regex(azureStorageQueueNameRegex, "Nom de queue invalide") - .describe("Nom de la queue à supprimer"), -}); - -export type DeleteStorageQueueParams = z.infer; - -// Schema pour envoyer un message storage -export const SendStorageMessageSchema = z.object({ - queueName: z.string() - .regex(azureStorageQueueNameRegex, "Nom de queue invalide") - .describe("Nom de la queue"), - messageText: z.string() - .min(1, "Le texte du message ne peut pas être vide") - .max(65536, "Le message ne peut pas dépasser 64KB") - .describe("Texte du message à envoyer"), - visibilityTimeoutInSeconds: z.number().min(1).max(604800).optional().describe("Délai de visibilité en secondes (1-604800)"), - messageTimeToLiveInSeconds: z.number().min(1).max(604800).optional().describe("TTL du message en secondes (1-604800)"), -}); - -export type SendStorageMessageParams = z.infer; - -// Schema pour recevoir des messages storage -export const ReceiveStorageMessagesSchema = z.object({ - queueName: z.string() - .regex(azureStorageQueueNameRegex, "Nom de queue invalide") - .describe("Nom de la queue"), - numberOfMessages: z.number().min(1).max(32).optional().describe("Nombre de messages à recevoir (1-32)"), - visibilityTimeoutInSeconds: z.number().min(1).max(43200).optional().describe("Délai de visibilité en secondes (1-43200)"), -}); - -export type ReceiveStorageMessagesParams = z.infer; - -// Schema pour aperçu des messages storage -export const PeekStorageMessagesSchema = z.object({ - queueName: z.string() - .regex(azureStorageQueueNameRegex, "Nom de queue invalide") - .describe("Nom de la queue"), - numberOfMessages: z.number().min(1).max(32).optional().describe("Nombre de messages à apercevoir (1-32)"), -}); - -export type PeekStorageMessagesParams = z.infer; - -// Schema pour supprimer un message storage -export const DeleteStorageMessageSchema = z.object({ - queueName: z.string() - .regex(azureStorageQueueNameRegex, "Nom de queue invalide") - .describe("Nom de la queue"), - messageId: z.string().describe("ID du message à supprimer"), - popReceipt: z.string().describe("Pop receipt du message"), -}); - -export type DeleteStorageMessageParams = z.infer; - -// Schema pour obtenir les propriétés d'une queue storage -export const GetStorageQueuePropertiesSchema = z.object({ - queueName: z.string() - .regex(azureStorageQueueNameRegex, "Nom de queue invalide") - .describe("Nom de la queue"), -}); - -export type GetStorageQueuePropertiesParams = z.infer; - -// Schema pour vider une queue storage -export const ClearStorageQueueSchema = z.object({ - queueName: z.string() - .regex(azureStorageQueueNameRegex, "Nom de queue invalide") - .describe("Nom de la queue à vider"), -}); +export type GetDocumentParams = z.infer; -export type ClearStorageQueueParams = z.infer; \ No newline at end of file +// Suggest Parameters +export const SuggestSchema = z.object({ + indexName: z.string().min(1).describe("Name of the search index"), + searchText: z.string().describe("Text to get suggestions for"), + suggesterName: z.string().describe("Name of the suggester to use"), + fuzzy: z.boolean().optional().default(false).describe("Enable fuzzy matching"), + highlightPreTag: z.string().optional().default("").describe("Pre-tag for highlighting"), + highlightPostTag: z.string().optional().default("").describe("Post-tag for highlighting"), + minimumCoverage: z.number().min(0).max(100).optional().describe("Minimum coverage percentage"), + orderBy: z.array(z.string()).optional().describe("Sort order"), + searchFields: z.array(z.string()).optional().describe("Fields to search in"), + select: z.array(z.string()).optional().describe("Fields to include in results"), + top: z.number().min(1).max(100).optional().default(5).describe("Number of suggestions to return"), + filter: z.string().optional().describe("OData filter expression"), +}); + +export type SuggestParams = z.infer; + +// Autocomplete Parameters +export const AutocompleteSchema = z.object({ + indexName: z.string().min(1).describe("Name of the search index"), + searchText: z.string().describe("Text to autocomplete"), + suggesterName: z.string().describe("Name of the suggester to use"), + autocompleteMode: z.enum(["oneTerm", "twoTerms", "oneTermWithContext"]).optional().default("oneTerm").describe("Autocomplete mode"), + fuzzy: z.boolean().optional().default(false).describe("Enable fuzzy matching"), + highlightPreTag: z.string().optional().default("").describe("Pre-tag for highlighting"), + highlightPostTag: z.string().optional().default("").describe("Post-tag for highlighting"), + minimumCoverage: z.number().min(0).max(100).optional().describe("Minimum coverage percentage"), + searchFields: z.array(z.string()).optional().describe("Fields to search in"), + top: z.number().min(1).max(100).optional().default(5).describe("Number of autocomplete terms to return"), + filter: z.string().optional().describe("OData filter expression"), +}); + +export type AutocompleteParams = z.infer; + +// Index Management +export const ListIndexesSchema = z.object({ + select: z.array(z.string()).optional().describe("Fields to include in results"), +}); + +export type ListIndexesParams = z.infer; + +export const GetIndexSchema = z.object({ + indexName: z.string().min(1).describe("Name of the index"), +}); + +export type GetIndexParams = z.infer; + +export const GetIndexStatisticsSchema = z.object({ + indexName: z.string().min(1).describe("Name of the index"), +}); + +export type GetIndexStatisticsParams = z.infer; + +// Document Management +export const UploadDocumentsSchema = z.object({ + indexName: z.string().min(1).describe("Name of the search index"), + documents: z.array(z.record(z.any())).min(1).max(1000).describe("Documents to upload (max 1000)"), +}); + +export type UploadDocumentsParams = z.infer; + +export const MergeDocumentsSchema = z.object({ + indexName: z.string().min(1).describe("Name of the search index"), + documents: z.array(z.record(z.any())).min(1).max(1000).describe("Documents to merge (max 1000)"), +}); + +export type MergeDocumentsParams = z.infer; + +export const DeleteDocumentsSchema = z.object({ + indexName: z.string().min(1).describe("Name of the search index"), + keyField: z.string().describe("Name of the key field"), + keyValues: z.array(z.string()).min(1).max(1000).describe("Key values of documents to delete (max 1000)"), +}); + +export type DeleteDocumentsParams = z.infer; + +// Response Types +export interface SearchResult { + success: boolean; + data?: { + results: T[]; + count?: number; + facets?: Record; + coverage?: number; + nextPageParameters?: any; + }; + error?: string; +} + +export interface DocumentResult { + success: boolean; + data?: T; + error?: string; +} + +export interface SuggestResult { + success: boolean; + data?: { + results: Array<{ + text: string; + document: any; + }>; + coverage?: number; + }; + error?: string; +} + +export interface AutocompleteResult { + success: boolean; + data?: { + results: Array<{ + text: string; + queryPlusText: string; + }>; + coverage?: number; + }; + error?: string; +} + +export interface IndexResult { + success: boolean; + data?: any; + error?: string; +} + +export interface BatchResult { + success: boolean; + data?: { + results: Array<{ + key: string; + status: boolean; + errorMessage?: string; + }>; + }; + error?: string; +} + +// Phase 4: Vector Search Schemas +export const VectorQuerySchema = z.object({ + vector: z.array(z.number()).describe("Vector values for similarity search"), + fields: z.string().describe("Vector field name to search"), + k: z.number().min(1).max(1000).optional().default(50).describe("Number of nearest neighbors to return"), + exhaustive: z.boolean().optional().default(false).describe("Use exhaustive search for higher accuracy"), +}); + +export type VectorQuery = z.infer; + +export const VectorSearchSchema = z.object({ + indexName: z.string().min(1).describe("Name of the search index"), + vectorQueries: z.array(VectorQuerySchema).min(1).describe("Vector queries to execute"), + select: z.array(z.string()).optional().describe("Fields to include in results"), + filter: z.string().optional().describe("OData filter expression"), + top: z.number().min(1).max(1000).optional().default(50).describe("Number of results to return"), + skip: z.number().min(0).optional().describe("Number of results to skip"), +}); + +export type VectorSearchParams = z.infer; + +export const HybridSearchSchema = z.object({ + indexName: z.string().min(1).describe("Name of the search index"), + searchText: z.string().describe("Text query for hybrid search"), + vectorQueries: z.array(VectorQuerySchema).min(1).describe("Vector queries for hybrid search"), + searchMode: z.enum(["any", "all"]).optional().default("any").describe("Text search mode"), + searchFields: z.array(z.string()).optional().describe("Text fields to search in"), + select: z.array(z.string()).optional().describe("Fields to include in results"), + filter: z.string().optional().describe("OData filter expression"), + orderBy: z.array(z.string()).optional().describe("Sort order"), + top: z.number().min(1).max(1000).optional().default(50).describe("Number of results to return"), + skip: z.number().min(0).optional().describe("Number of results to skip"), + queryType: z.enum(["simple", "full"]).optional().default("simple").describe("Text query type"), +}); + +export type HybridSearchParams = z.infer; + +// Phase 4: Semantic Search Schemas +export const SemanticSearchSchema = z.object({ + indexName: z.string().min(1).describe("Name of the search index"), + searchText: z.string().describe("Text to search semantically"), + semanticConfiguration: z.string().describe("Name of the semantic configuration to use"), + searchFields: z.array(z.string()).optional().describe("Fields to search in"), + select: z.array(z.string()).optional().describe("Fields to include in results"), + filter: z.string().optional().describe("OData filter expression"), + orderBy: z.array(z.string()).optional().describe("Sort order"), + top: z.number().min(1).max(1000).optional().default(50).describe("Number of results to return"), + skip: z.number().min(0).optional().describe("Number of results to skip"), + answers: z.object({ + answerType: z.enum(["extractive"]).default("extractive"), + count: z.number().min(1).max(10).optional().default(3).describe("Number of answers to generate"), + threshold: z.number().min(0).max(1).optional().default(0.7).describe("Confidence threshold for answers"), + }).optional().describe("Semantic answers configuration"), + captions: z.object({ + captionType: z.enum(["extractive"]).default("extractive"), + maxTextRecordsToProcess: z.number().min(1).max(1000).optional().default(1000), + highlight: z.boolean().optional().default(true).describe("Enable highlighting in captions"), + }).optional().describe("Semantic captions configuration"), +}); + +export type SemanticSearchParams = z.infer; + +// Phase 4: Response Types for Vector and Semantic Search +export interface VectorSearchResult { + success: boolean; + data?: { + results: T[]; + count?: number; + }; + error?: string; +} + +export interface SemanticSearchResult { + success: boolean; + data?: { + results: T[]; + count?: number; + answers?: Array<{ + key: string; + text: string; + highlights: string; + score: number; + }>; + captions?: Array<{ + text: string; + highlights: string; + }>; + }; + error?: string; +} \ No newline at end of file