├── .env.example ├── .gitignore ├── .prettierignore ├── .prettierrc ├── Dockerfile ├── LICENSE ├── README.md ├── azure-pipelines.yml ├── glama.json ├── package-lock.json ├── package.json ├── smithery.yaml ├── src ├── index.ts ├── resources.ts └── tools.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | GRAPHLIT_ORGANIZATION_ID= 2 | GRAPHLIT_ENVIRONMENT_ID= 3 | GRAPHLIT_JWT_SECRET= 4 | 5 | SLACK_BOT_TOKEN= 6 | 7 | DISCORD_BOT_TOKEN= 8 | 9 | TWITTER_TOKEN= 10 | 11 | MICROSOFT_TEAMS_CLIENT_ID= 12 | MICROSOFT_TEAMS_CLIENT_SECRET= 13 | MICROSOFT_TEAMS_REFRESH_TOKEN= 14 | 15 | GOOGLE_EMAIL_REFRESH_TOKEN= 16 | GOOGLE_EMAIL_CLIENT_ID= 17 | GOOGLE_EMAIL_CLIENT_SECRET= 18 | 19 | LINEAR_API_KEY= 20 | 21 | GITHUB_PERSONAL_ACCESS_TOKEN= 22 | 23 | JIRA_EMAIL= 24 | JIRA_TOKEN= 25 | 26 | NOTION_API_KEY= 27 | 28 | GOOGLE_DRIVE_FOLDER_ID= 29 | GOOGLE_DRIVE_REFRESH_TOKEN= 30 | GOOGLE_DRIVE_CLIENT_ID= 31 | GOOGLE_DRIVE_CLIENT_SECRET= 32 | GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON= 33 | 34 | ONEDRIVE_FOLDER_ID= 35 | ONEDRIVE_REFRESH_TOKEN= 36 | ONEDRIVE_CLIENT_ID= 37 | ONEDRIVE_CLIENT_SECRET= 38 | 39 | SHAREPOINT_ACCOUNT_NAME= 40 | SHAREPOINT_REFRESH_TOKEN= 41 | SHAREPOINT_CLIENT_ID= 42 | SHAREPOINT_CLIENT_SECRET= 43 | 44 | DROPBOX_APP_KEY= 45 | DROPBOX_APP_SECRET= 46 | DROPBOX_REFRESH_TOKEN= 47 | DROPBOX_REDIRECT_URI= 48 | 49 | BOX_CLIENT_ID= 50 | BOX_CLIENT_SECRET= 51 | BOX_REFRESH_TOKEN= 52 | BOX_REDIRECT_URI= 53 | 54 | TWITTER_CONSUMER_API_KEY= 55 | TWITTER_CONSUMER_API_SECRET= 56 | TWITTER_ACCESS_TOKEN_KEY= 57 | TWITTER_ACCESS_TOKEN_SECRET= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | build 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Build output 5 | build/ 6 | dist/ 7 | 8 | # Package files 9 | package-lock.json 10 | 11 | # Environment files 12 | .env 13 | .env.* 14 | 15 | # Logs 16 | *.log 17 | 18 | # IDE files 19 | .vscode/ 20 | .idea/ 21 | 22 | # OS generated files 23 | .DS_Store 24 | Thumbs.db 25 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "singleQuote": false, 5 | "printWidth": 80, 6 | "tabWidth": 2, 7 | "useTabs": false 8 | } 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | FROM node:lts-alpine 3 | 4 | # Create app directory 5 | WORKDIR /usr/src/app 6 | 7 | # Install app dependencies 8 | COPY package*.json ./ 9 | RUN npm install --production --ignore-scripts 10 | 11 | # Copy app source code 12 | COPY . . 13 | 14 | # Build the project 15 | RUN npm run build 16 | 17 | # Expose port if needed (adjust if your server listens on a port) 18 | # EXPOSE 3000 19 | 20 | # Start the server 21 | CMD ["node", "build/index.js"] 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Unstruk Data Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/graphlit-mcp-server.svg)](https://badge.fury.io/js/graphlit-mcp-server) 2 | [![smithery badge](https://smithery.ai/badge/@graphlit/graphlit-mcp-server)](https://smithery.ai/server/@graphlit/graphlit-mcp-server) 3 | 4 | # Model Context Protocol (MCP) Server for Graphlit Platform 5 | 6 | ## Overview 7 | 8 | The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. This document outlines the setup process and provides a basic example of using the client. 9 | 10 | Ingest anything from Slack, Discord, websites, Google Drive, email, Jira, Linear or GitHub into a Graphlit project - and then search and retrieve relevant knowledge within an MCP client like Cursor, Windsurf, Goose or Cline. 11 | 12 | Your Graphlit project acts as a searchable, and RAG-ready knowledge base across all your developer and product management tools. 13 | 14 | Documents (PDF, DOCX, PPTX, etc.) and HTML web pages will be extracted to Markdown upon ingestion. Audio and video files will be transcribed upon ingestion. 15 | 16 | Web crawling and web search are built-in as MCP tools, with no need to integrate other tools like Firecrawl, Exa, etc. separately. 17 | 18 | You can read more about the MCP Server use cases and features on our [blog](https://www.graphlit.com/blog/graphlit-mcp-server). 19 | 20 | Watch our latest [YouTube video](https://www.youtube.com/watch?v=Or-QqonvcAs&t=4s) on using the Graphlit MCP Server with the Goose MCP client. 21 | 22 | For any questions on using the MCP Server, please join our [Discord](https://discord.gg/ygFmfjy3Qx) community and post on the #mcp channel. 23 | 24 | 25 | graphlit-mcp-server MCP server 26 | 27 | 28 | ## Tools 29 | 30 | ### Retrieval 31 | 32 | - Query Contents 33 | - Query Collections 34 | - Query Feeds 35 | - Query Conversations 36 | - Retrieve Relevant Sources 37 | - Retrieve Similar Images 38 | - Visually Describe Image 39 | 40 | ### RAG 41 | 42 | - Prompt LLM Conversation 43 | 44 | ### Extraction 45 | 46 | - Extract Structured JSON from Text 47 | 48 | ### Publishing 49 | 50 | - Publish as Audio (ElevenLabs Audio) 51 | - Publish as Image (OpenAI Image Generation) 52 | 53 | ### Ingestion 54 | 55 | - Files 56 | - Web Pages 57 | - Messages 58 | - Posts 59 | - Emails 60 | - Issues 61 | - Text 62 | - Memory (Short-Term) 63 | 64 | ### Data Connectors 65 | 66 | - Microsoft Outlook email 67 | - Google Mail 68 | - Notion 69 | - Reddit 70 | - Linear 71 | - Jira 72 | - GitHub Issues 73 | - Google Drive 74 | - OneDrive 75 | - SharePoint 76 | - Dropbox 77 | - Box 78 | - GitHub 79 | - Slack 80 | - Microsoft Teams 81 | - Discord 82 | - Twitter/X 83 | - Podcasts (RSS) 84 | 85 | ### Web 86 | 87 | - Web Crawling 88 | - Web Search (including Podcast Search) 89 | - Web Mapping 90 | - Screenshot Page 91 | 92 | ### Notifications 93 | 94 | - Slack 95 | - Email 96 | - Webhook 97 | - Twitter/X 98 | 99 | ### Operations 100 | 101 | - Configure Project 102 | - Create Collection 103 | - Add Contents to Collection 104 | - Remove Contents from Collection 105 | - Delete Collection(s) 106 | - Delete Feed(s) 107 | - Delete Content(s) 108 | - Delete Conversation(s) 109 | - Is Feed Done? 110 | - Is Content Done? 111 | 112 | ### Enumerations 113 | 114 | - List Slack Channels 115 | - List Microsoft Teams Teams 116 | - List Microsoft Teams Channels 117 | - List SharePoint Libraries 118 | - List SharePoint Folders 119 | - List Linear Projects 120 | - List Notion Databases 121 | 122 | ## Resources 123 | 124 | - Project 125 | - Contents 126 | - Feeds 127 | - Collections (of Content) 128 | - Workflows 129 | - Conversations 130 | - Specifications 131 | 132 | ## Prerequisites 133 | 134 | Before you begin, ensure you have the following: 135 | 136 | - Node.js installed on your system (recommended version 18.x or higher). 137 | - An active account on the [Graphlit Platform](https://portal.graphlit.dev) with access to the API settings dashboard. 138 | 139 | ## Configuration 140 | 141 | The Graphlit MCP Server supports environment variables to be set for authentication and configuration: 142 | 143 | - `GRAPHLIT_ENVIRONMENT_ID`: Your environment ID. 144 | - `GRAPHLIT_ORGANIZATION_ID`: Your organization ID. 145 | - `GRAPHLIT_JWT_SECRET`: Your JWT secret for signing the JWT token. 146 | 147 | You can find these values in the API settings dashboard on the [Graphlit Platform](https://portal.graphlit.dev). 148 | 149 | ## Installation 150 | 151 | ### Installing via VS Code 152 | 153 | For quick installation, use one of the one-click install buttons below: 154 | 155 | [![Install with NPX in VS Code](https://img.shields.io/badge/VS_Code-NPM-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=graphlit&inputs=%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22organization_id%22%2C%22description%22%3A%22Graphlit%20Organization%20ID%22%2C%22password%22%3Atrue%7D%2C%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22environment_id%22%2C%22description%22%3A%22Graphlit%20Environment%20ID%22%2C%22password%22%3Atrue%7D%2C%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22jwt_secret%22%2C%22description%22%3A%22Graphlit%20JWT%20Secret%22%2C%22password%22%3Atrue%7D%5D&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22graphlit-mcp-server%22%5D%2C%22env%22%3A%7B%22GRAPHLIT_ORGANIZATION_ID%22%3A%22%24%7Binput%3Aorganization_id%7D%22%2C%22GRAPHLIT_ENVIRONMENT_ID%22%3A%22%24%7Binput%3Aenvironment_id%7D%22%2C%22GRAPHLIT_JWT_SECRET%22%3A%22%24%7Binput%3Ajwt_secret%7D%22%7D%7D) [![Install with NPX in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-NPM-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=graphlit&inputs=%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22organization_id%22%2C%22description%22%3A%22Graphlit%20Organization%20ID%22%2C%22password%22%3Atrue%7D%2C%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22environment_id%22%2C%22description%22%3A%22Graphlit%20Environment%20ID%22%2C%22password%22%3Atrue%7D%2C%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22jwt_secret%22%2C%22description%22%3A%22Graphlit%20JWT%20Secret%22%2C%22password%22%3Atrue%7D%5D&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22graphlit-mcp-server%22%5D%2C%22env%22%3A%7B%22GRAPHLIT_ORGANIZATION_ID%22%3A%22%24%7Binput%3Aorganization_id%7D%22%2C%22GRAPHLIT_ENVIRONMENT_ID%22%3A%22%24%7Binput%3Aenvironment_id%7D%22%2C%22GRAPHLIT_JWT_SECRET%22%3A%22%24%7Binput%3Ajwt_secret%7D%22%7D%7D&quality=insiders) 156 | 157 | For manual installation, add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`. 158 | 159 | Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others. 160 | 161 | > Note that the `mcp` key is not needed in the `.vscode/mcp.json` file. 162 | 163 | ```json 164 | { 165 | "mcp": { 166 | "inputs": [ 167 | { 168 | "type": "promptString", 169 | "id": "organization_id", 170 | "description": "Graphlit Organization ID", 171 | "password": true 172 | }, 173 | { 174 | "type": "promptString", 175 | "id": "environment_id", 176 | "description": "Graphlit Environment ID", 177 | "password": true 178 | }, 179 | { 180 | "type": "promptString", 181 | "id": "jwt_secret", 182 | "description": "Graphlit JWT Secret", 183 | "password": true 184 | } 185 | ], 186 | "servers": { 187 | "graphlit": { 188 | "command": "npx", 189 | "args": ["-y", "graphlit-mcp-server"], 190 | "env": { 191 | "GRAPHLIT_ORGANIZATION_ID": "${input:organization_id}", 192 | "GRAPHLIT_ENVIRONMENT_ID": "${input:environment_id}", 193 | "GRAPHLIT_JWT_SECRET": "${input:jwt_secret}" 194 | } 195 | } 196 | } 197 | } 198 | } 199 | ``` 200 | 201 | ### Installing via Windsurf 202 | 203 | To install graphlit-mcp-server in Windsurf IDE application, Cline should use NPX: 204 | 205 | ```bash 206 | npx -y graphlit-mcp-server 207 | ``` 208 | 209 | Your mcp_config.json file should be configured similar to: 210 | 211 | ``` 212 | { 213 | "mcpServers": { 214 | "graphlit-mcp-server": { 215 | "command": "npx", 216 | "args": [ 217 | "-y", 218 | "graphlit-mcp-server" 219 | ], 220 | "env": { 221 | "GRAPHLIT_ORGANIZATION_ID": "your-organization-id", 222 | "GRAPHLIT_ENVIRONMENT_ID": "your-environment-id", 223 | "GRAPHLIT_JWT_SECRET": "your-jwt-secret", 224 | } 225 | } 226 | } 227 | } 228 | ``` 229 | 230 | ### Installing via Cline 231 | 232 | To install graphlit-mcp-server in Cline IDE application, Cline should use NPX: 233 | 234 | ```bash 235 | npx -y graphlit-mcp-server 236 | ``` 237 | 238 | Your cline_mcp_settings.json file should be configured similar to: 239 | 240 | ``` 241 | { 242 | "mcpServers": { 243 | "graphlit-mcp-server": { 244 | "command": "npx", 245 | "args": [ 246 | "-y", 247 | "graphlit-mcp-server" 248 | ], 249 | "env": { 250 | "GRAPHLIT_ORGANIZATION_ID": "your-organization-id", 251 | "GRAPHLIT_ENVIRONMENT_ID": "your-environment-id", 252 | "GRAPHLIT_JWT_SECRET": "your-jwt-secret", 253 | } 254 | } 255 | } 256 | } 257 | ``` 258 | 259 | ### Installing via Cursor 260 | 261 | To install graphlit-mcp-server in Cursor IDE application, Cursor should use NPX: 262 | 263 | ```bash 264 | npx -y graphlit-mcp-server 265 | ``` 266 | 267 | Your mcp.json file should be configured similar to: 268 | 269 | ``` 270 | { 271 | "mcpServers": { 272 | "graphlit-mcp-server": { 273 | "command": "npx", 274 | "args": [ 275 | "-y", 276 | "graphlit-mcp-server" 277 | ], 278 | "env": { 279 | "GRAPHLIT_ORGANIZATION_ID": "your-organization-id", 280 | "GRAPHLIT_ENVIRONMENT_ID": "your-environment-id", 281 | "GRAPHLIT_JWT_SECRET": "your-jwt-secret", 282 | } 283 | } 284 | } 285 | } 286 | ``` 287 | 288 | ### Installing via Smithery 289 | 290 | To install graphlit-mcp-server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@graphlit/graphlit-mcp-server): 291 | 292 | ```bash 293 | npx -y @smithery/cli install @graphlit/graphlit-mcp-server --client claude 294 | ``` 295 | 296 | ### Installing manually 297 | 298 | To use the Graphlit MCP Server in any MCP client application, use: 299 | 300 | ``` 301 | { 302 | "mcpServers": { 303 | "graphlit-mcp-server": { 304 | "command": "npx", 305 | "args": [ 306 | "-y", 307 | "graphlit-mcp-server" 308 | ], 309 | "env": { 310 | "GRAPHLIT_ORGANIZATION_ID": "your-organization-id", 311 | "GRAPHLIT_ENVIRONMENT_ID": "your-environment-id", 312 | "GRAPHLIT_JWT_SECRET": "your-jwt-secret", 313 | } 314 | } 315 | } 316 | } 317 | ``` 318 | 319 | Optionally, you can configure the credentials for data connectors, such as Slack, Google Email and Notion. 320 | Only GRAPHLIT_ORGANIZATION_ID, GRAPHLIT_ENVIRONMENT_ID and GRAPHLIT_JWT_SECRET are required. 321 | 322 | ``` 323 | { 324 | "mcpServers": { 325 | "graphlit-mcp-server": { 326 | "command": "npx", 327 | "args": [ 328 | "-y", 329 | "graphlit-mcp-server" 330 | ], 331 | "env": { 332 | "GRAPHLIT_ORGANIZATION_ID": "your-organization-id", 333 | "GRAPHLIT_ENVIRONMENT_ID": "your-environment-id", 334 | "GRAPHLIT_JWT_SECRET": "your-jwt-secret", 335 | "SLACK_BOT_TOKEN": "your-slack-bot-token", 336 | "DISCORD_BOT_TOKEN": "your-discord-bot-token", 337 | "TWITTER_TOKEN": "your-twitter-token", 338 | "GOOGLE_EMAIL_REFRESH_TOKEN": "your-google-refresh-token", 339 | "GOOGLE_EMAIL_CLIENT_ID": "your-google-client-id", 340 | "GOOGLE_EMAIL_CLIENT_SECRET": "your-google-client-secret", 341 | "LINEAR_API_KEY": "your-linear-api-key", 342 | "GITHUB_PERSONAL_ACCESS_TOKEN": "your-github-pat", 343 | "JIRA_EMAIL": "your-jira-email", 344 | "JIRA_TOKEN": "your-jira-token", 345 | "NOTION_API_KEY": "your-notion-api-key" 346 | } 347 | } 348 | } 349 | } 350 | ``` 351 | 352 | NOTE: when running 'npx' on Windows, you may need to explicitly call npx via the command prompt. 353 | 354 | ``` 355 | "command": "C:\\Windows\\System32\\cmd.exe /c npx" 356 | ``` 357 | 358 | ## Support 359 | 360 | Please refer to the [Graphlit API Documentation](https://docs.graphlit.dev/). 361 | 362 | For support with the Graphlit MCP Server, please submit a [GitHub Issue](https://github.com/graphlit/graphlit-mcp-server/issues). 363 | 364 | For further support with the Graphlit Platform, please join our [Discord](https://discord.gg/ygFmfjy3Qx) community. 365 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - main 5 | 6 | name: $(Date:yyyyMMdd)$(Rev:rrr) 7 | 8 | pool: 9 | vmImage: "ubuntu-latest" 10 | 11 | steps: 12 | - task: NodeTool@0 13 | inputs: 14 | versionSpec: "20.x" 15 | displayName: "Install Node.js" 16 | 17 | - script: | 18 | npm install 19 | npm run build 20 | npm version 1.0.$(Build.BuildNumber) --no-git-tag-version --allow-same-version 21 | displayName: "Install dependencies and build" 22 | 23 | - task: CopyFiles@2 24 | inputs: 25 | SourceFolder: "$(Build.SourcesDirectory)" 26 | Contents: "README.md" 27 | TargetFolder: "$(Build.ArtifactStagingDirectory)/build" 28 | 29 | - task: CopyFiles@2 30 | inputs: 31 | SourceFolder: "$(Build.SourcesDirectory)" 32 | Contents: "LICENSE" 33 | TargetFolder: "$(Build.ArtifactStagingDirectory)/build" 34 | 35 | - task: Npm@1 36 | inputs: 37 | command: publish 38 | publishRegistry: useExternalRegistry 39 | publishEndpoint: "NPM" 40 | -------------------------------------------------------------------------------- /glama.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://glama.ai/mcp/schemas/server.json", 3 | "maintainers": ["kirk-marple"] 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphlit-mcp-server", 3 | "version": "1.0.0", 4 | "description": "Graphlit MCP Server", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/graphlit/graphlit-mcp-server.git" 9 | }, 10 | "contributors": [ 11 | "Kirk Marple (https://github.com/kirk-marple)" 12 | ], 13 | "bin": { 14 | "graphlit-mcp-server": "build/index.js" 15 | }, 16 | "files": [ 17 | "build" 18 | ], 19 | "engines": { 20 | "node": ">=18.0.0" 21 | }, 22 | "scripts": { 23 | "build": "tsc -p tsconfig.json", 24 | "watch": "tsc --watch", 25 | "check": "tsc --noEmit --incremental", 26 | "format": "prettier --write .", 27 | "inspector": "npx @modelcontextprotocol/inspector node build/index.js" 28 | }, 29 | "keywords": [ 30 | "Graphlit", 31 | "API", 32 | "LLM", 33 | "AI", 34 | "RAG", 35 | "OpenAI", 36 | "PDF", 37 | "parsing", 38 | "preprocessing", 39 | "memory", 40 | "agents", 41 | "agent tools", 42 | "retrieval", 43 | "web scraping", 44 | "knowledge graph", 45 | "MCP" 46 | ], 47 | "author": "Unstruk Data Inc.", 48 | "license": "MIT", 49 | "dependencies": { 50 | "@modelcontextprotocol/sdk": "^1.11.4", 51 | "graphlit-client": "^1.0.20250531005" 52 | }, 53 | "devDependencies": { 54 | "@types/mime-types": "^2.1.4", 55 | "prettier": "^3.5.3", 56 | "typescript": "^5.8.2" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: 9 | - organizationId 10 | - environmentId 11 | - jwtSecret 12 | properties: 13 | organizationId: 14 | type: string 15 | default: your-organization-id 16 | description: Graphlit organization ID 17 | environmentId: 18 | type: string 19 | default: your-environment-id 20 | description: Graphlit environment ID 21 | jwtSecret: 22 | type: string 23 | default: your-jwt-secret 24 | description: JWT secret for signing tokens 25 | slackBotToken: 26 | type: string 27 | default: "" 28 | description: Slack bot token (optional) 29 | discordBotToken: 30 | type: string 31 | default: "" 32 | description: Discord bot token (optional) 33 | googleEmailRefreshToken: 34 | type: string 35 | default: "" 36 | description: Google Email refresh token (optional) 37 | googleEmailClientId: 38 | type: string 39 | default: "" 40 | description: Google Email Client ID (optional) 41 | googleEmailClientSecret: 42 | type: string 43 | default: "" 44 | description: Google Email Client Secret (optional) 45 | linearApiKey: 46 | type: string 47 | default: "" 48 | description: Linear API Key (optional) 49 | githubPersonalAccessToken: 50 | type: string 51 | default: "" 52 | description: GitHub Personal Access Token (optional) 53 | jiraEmail: 54 | type: string 55 | default: "" 56 | description: Jira email (optional) 57 | jiraToken: 58 | type: string 59 | default: "" 60 | description: Jira token (optional) 61 | notionApiKey: 62 | type: string 63 | default: "" 64 | description: Notion API Key (optional) 65 | twitterConsumerApiKey: 66 | type: string 67 | default: "" 68 | description: Twitter Consumer API Key (optional) 69 | twitterConsumerApiSecret: 70 | type: string 71 | default: "" 72 | description: Twitter Consumer API Secret (optional) 73 | twitterAccessTokenKey: 74 | type: string 75 | default: "" 76 | description: Twitter Access Token Key (optional) 77 | twitterAccessTokenSecret: 78 | type: string 79 | default: "" 80 | description: Twitter Access Token Secret (optional) 81 | commandFunction: 82 | # A JS function that produces the CLI command based on the given config to start the MCP on stdio. 83 | |- 84 | (config) => ({ 85 | command: 'node', 86 | args: ['build/index.js'], 87 | env: { 88 | GRAPHLIT_ORGANIZATION_ID: config.organizationId, 89 | GRAPHLIT_ENVIRONMENT_ID: config.environmentId, 90 | GRAPHLIT_JWT_SECRET: config.jwtSecret, 91 | SLACK_BOT_TOKEN: config.slackBotToken || '', 92 | DISCORD_BOT_TOKEN: config.discordBotToken || '', 93 | GOOGLE_EMAIL_REFRESH_TOKEN: config.googleEmailRefreshToken || '', 94 | GOOGLE_EMAIL_CLIENT_ID: config.googleEmailClientId || '', 95 | GOOGLE_EMAIL_CLIENT_SECRET: config.googleEmailClientSecret || '', 96 | LINEAR_API_KEY: config.linearApiKey || '', 97 | GITHUB_PERSONAL_ACCESS_TOKEN: config.githubPersonalAccessToken || '', 98 | JIRA_EMAIL: config.jiraEmail || '', 99 | JIRA_TOKEN: config.jiraToken || '', 100 | NOTION_API_KEY: config.notionApiKey || '', 101 | TWITTER_CONSUMER_API_KEY: config.twitterConsumerApiKey || '', 102 | TWITTER_CONSUMER_API_SECRET: config.twitterConsumerApiSecret || '', 103 | TWITTER_ACCESS_TOKEN_KEY: config.twitterAccessTokenKey || '', 104 | TWITTER_ACCESS_TOKEN_SECRET: config.twitterAccessTokenSecret || '', 105 | } 106 | }) 107 | exampleConfig: 108 | organizationId: your-organization-id 109 | environmentId: your-environment-id 110 | jwtSecret: your-jwt-secret 111 | slackBotToken: example-slack-bot-token 112 | discordBotToken: example-discord-bot-token 113 | googleEmailRefreshToken: example-google-refresh-token 114 | googleEmailClientId: example-google-client-id 115 | googleEmailClientSecret: example-google-client-secret 116 | linearApiKey: example-linear-api-key 117 | githubPersonalAccessToken: example-github-pat 118 | jiraEmail: example-jira-email 119 | jiraToken: example-jira-token 120 | notionApiKey: example-notion-api-key, 121 | twitterConsumerApiKey: example-twitter-consumer-api-key 122 | twitterConsumerApiSecret: example-twitter-consumer-api-secret 123 | twitterAccessTokenKey: example-twitter-access-token-key 124 | twitterAccessTokenSecret: example-twitter-access-token-secret 125 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 4 | import { registerResources } from "./resources.js"; 5 | import { registerTools } from "./tools.js"; 6 | 7 | const DEFAULT_INSTRUCTIONS = ` 8 | You are provided a set of MCP tools and resources that integrate with the [Graphlit](https://www.graphlit.com) Platform. 9 | 10 | To use each of the Graphlit MCP tools, there may be environment variables which are required to be configured in your MCP client. These are described in the description for each tool. 11 | These must be configured in the MCP client YAML or JSON configuration file before you can use the tools. *Do not* set these directly in your Terminal or shell environment. 12 | 13 | Graphlit is an LLM-enabled knowledge API platform, which supports these resources: 14 | - project: container for ingested contents, which can be configured with a default workflow 15 | - contents: all ingested files, web pages, messages, etc.; also includes short-term 'memory' contents 16 | - feeds: data connectors which ingest contents 17 | - collections: named groups of contents 18 | - conversations: chat message history of LLM conversation, which uses RAG pipeline for content retrieval 19 | - workflows: how content is handled during the ingestion process 20 | - specifications: LLM configuration presets, used by workflows and conversations 21 | 22 | Identifiers for all resources are unique within the Graphlit project, and are formatted as GUIDs. 23 | 24 | You have access to one and only one Graphlit project, which can optionally be configured with a workflow to guide the document preparation and entity extraction of ingested content. 25 | The Graphlit project is non-deletable, but you can create and delete contents, feeds, collections, conversations, specifications and workflows within the project. 26 | 27 | You can query the Graphlit project resource for the credits used, LLM tokens used, and the available project quota. By default, credits cost USD$0.10, and are discounted on higher paid tiers. 28 | 29 | With this Graphlit MCP Server, you can ingest anything from Slack, Discord, websites, Notion, Google Drive, email, Jira, Linear or GitHub into a Graphlit project - and then search and retrieve relevant knowledge within an MCP client like Cursor, Windsurf or Cline. 30 | 31 | Documents (PDF, DOCX, PPTX, etc.) and HTML web pages will be extracted to Markdown upon ingestion. Audio and video files will be transcribed upon ingestion. 32 | 33 | ## Best Practices: 34 | 1. Always look for matching resources before you try to call any tools. 35 | For example, "have i configured any graphlit workflows?", you should check for workflow resources before trying to call any other tools. 36 | 2. Don't use 'retrieveSources' to locate contents, when you have already added the contents into a collection. In that case, first retrieve the collection resource, which contains the content resources. 37 | 3. Only call the 'configureProject' tool when the user explicitly asks to configure their Graphlit project defaults. 38 | 4. Never infer, guess at or hallucinate any URLs. Always retrieve the latest content resources in order to get downloadable URLs. 39 | 5. Use 'ingestMemory' to save short-term memories, such as temporary notes or intermediate state for research. Use 'ingestText' to store long-term knowledge, such as Markdown results from research. 40 | 6. Always use 'PODSCAN' web search type when searching for podcast episodes, podcast appearances, etc. 41 | 7. Prioritize using feeds, rather than 'ingestUrl', when you want to ingest a website. Feeds are more efficient and faster than using 'ingestUrl'. 42 | If you receive a request to ingest a GitHub URL, use the 'ingestGitHubFiles' tool to ingest the repository, rather than using 'ingestUrl'. 43 | Always attempt to use the most-specific tool for the task at hand. 44 | 45 | ## Short-term vs Long-term Memory: 46 | You can perform scatter-gather operations where you save short-term memories after each workflow step, and then gather relevant memories prior to the moving onto the next step. 47 | Leverage short-term memories when evaluating the results of a workflow step, and then use long-term memories to store the final results of your workflow. 48 | You can collect memories in collections, and then use the 'queryContents' tool to retrieve the 'memory' contents by the collection. This will help you to keep track of your progress and avoid losing any important information. 49 | 50 | If you have any trouble with this Graphlit MCP Server, join our [Discord](https://discord.gg/ygFmfjy3Qx) community for support. 51 | `; 52 | 53 | export const server = new McpServer( 54 | { 55 | name: "Graphlit MCP Server", 56 | version: "1.0.0", 57 | }, 58 | { 59 | instructions: DEFAULT_INSTRUCTIONS, 60 | } 61 | ); 62 | 63 | registerResources(server); 64 | registerTools(server); 65 | 66 | async function runServer() { 67 | try { 68 | console.error("Attempting to start Graphlit MCP Server."); 69 | 70 | const transport = new StdioServerTransport(); 71 | await server.connect(transport); 72 | 73 | console.error("Successfully started Graphlit MCP Server."); 74 | } catch (error) { 75 | console.error("Failed to start Graphlit MCP Server.", error); 76 | 77 | process.exit(1); 78 | } 79 | } 80 | 81 | runServer().catch((error) => { 82 | console.error("Failed to start Graphlit MCP Server.", error); 83 | 84 | process.exit(1); 85 | }); 86 | -------------------------------------------------------------------------------- /src/resources.ts: -------------------------------------------------------------------------------- 1 | import { Graphlit } from "graphlit-client"; 2 | import { 3 | McpServer, 4 | ResourceTemplate, 5 | } from "@modelcontextprotocol/sdk/server/mcp.js"; 6 | import { 7 | ContentTypes, 8 | ContentFilter, 9 | ConversationFilter, 10 | EntityState, 11 | GetContentQuery, 12 | GetConversationQuery, 13 | } from "graphlit-client/dist/generated/graphql-types.js"; 14 | 15 | export function registerResources(server: McpServer) { 16 | server.resource( 17 | "Conversations list: Returns list of conversation resources.", 18 | new ResourceTemplate("conversations://", { 19 | list: async (extra) => { 20 | const client = new Graphlit(); 21 | 22 | const filter: ConversationFilter = {}; 23 | 24 | try { 25 | const response = await client.queryConversations(filter); 26 | 27 | return { 28 | resources: (response.conversations?.results || []) 29 | .filter((content) => content !== null) 30 | .map((conversation) => ({ 31 | name: conversation.name, 32 | uri: `conversations://${conversation.id}`, 33 | mimeType: "text/markdown", 34 | })), 35 | }; 36 | } catch (error) { 37 | console.error("Error fetching conversation list:", error); 38 | return { resources: [] }; 39 | } 40 | }, 41 | }), 42 | async (uri, variables) => { 43 | return { 44 | contents: [], 45 | }; 46 | } 47 | ); 48 | 49 | server.resource( 50 | "Conversation: Returns LLM conversation messages. Accepts conversation resource URI, i.e. conversations://{id}, where 'id' is a conversation identifier.", 51 | new ResourceTemplate("conversations://{id}", { list: undefined }), 52 | async (uri: URL, variables) => { 53 | const id = variables.id as string; 54 | const client = new Graphlit(); 55 | 56 | try { 57 | const response = await client.getConversation(id); 58 | 59 | const content = response.conversation; 60 | 61 | return { 62 | contents: [ 63 | { 64 | uri: uri.toString(), 65 | text: formatConversation(response), 66 | mimeType: "text/markdown", 67 | }, 68 | ], 69 | }; 70 | } catch (error) { 71 | console.error("Error fetching conversation:", error); 72 | return { 73 | contents: [], 74 | }; 75 | } 76 | } 77 | ); 78 | 79 | server.resource( 80 | "Feeds: Returns list of feed resources.", 81 | new ResourceTemplate("feeds://", { 82 | list: async (extra) => { 83 | const client = new Graphlit(); 84 | 85 | try { 86 | const response = await client.queryFeeds(); 87 | 88 | return { 89 | resources: (response.feeds?.results || []) 90 | .filter((feed) => feed !== null) 91 | .map((feed) => ({ 92 | name: feed.name, 93 | uri: `feeds://${feed.id}`, 94 | })), 95 | }; 96 | } catch (error) { 97 | console.error("Error fetching feed list:", error); 98 | return { resources: [] }; 99 | } 100 | }, 101 | }), 102 | async (uri, variables) => { 103 | return { 104 | contents: [], 105 | }; 106 | } 107 | ); 108 | 109 | server.resource( 110 | "Feed: Returns feed metadata. Accepts content resource URI, i.e. feeds://{id}, where 'id' is a feed identifier.", 111 | new ResourceTemplate("feeds://{id}", { list: undefined }), 112 | async (uri: URL, variables) => { 113 | const id = variables.id as string; 114 | const client = new Graphlit(); 115 | 116 | try { 117 | const response = await client.getFeed(id); 118 | 119 | return { 120 | contents: [ 121 | { 122 | uri: uri.toString(), 123 | text: JSON.stringify( 124 | { 125 | id: response.feed?.id, 126 | name: response.feed?.name, 127 | type: response.feed?.type, 128 | readCount: response.feed?.readCount, 129 | creationDate: response.feed?.creationDate, 130 | lastReadDate: response.feed?.lastReadDate, 131 | state: response.feed?.state, 132 | error: response.feed?.error, 133 | }, 134 | null, 135 | 2 136 | ), 137 | mimeType: "application/json", 138 | }, 139 | ], 140 | }; 141 | } catch (error) { 142 | console.error("Error fetching feed:", error); 143 | return { 144 | contents: [], 145 | }; 146 | } 147 | } 148 | ); 149 | 150 | server.resource( 151 | "Collections: Returns list of collection resources.", 152 | new ResourceTemplate("collections://", { 153 | list: async (extra) => { 154 | const client = new Graphlit(); 155 | 156 | try { 157 | const response = await client.queryCollections(); 158 | 159 | return { 160 | resources: (response.collections?.results || []) 161 | .filter((collection) => collection !== null) 162 | .map((collection) => ({ 163 | name: collection.name, 164 | uri: `collections://${collection.id}`, 165 | })), 166 | }; 167 | } catch (error) { 168 | console.error("Error fetching collection list:", error); 169 | return { resources: [] }; 170 | } 171 | }, 172 | }), 173 | async (uri, variables) => { 174 | return { 175 | contents: [], 176 | }; 177 | } 178 | ); 179 | 180 | server.resource( 181 | "Collection: Returns collection metadata and list of content resources. Accepts collection resource URI, i.e. collections://{id}, where 'id' is a collection identifier.", 182 | new ResourceTemplate("collections://{id}", { list: undefined }), 183 | async (uri: URL, variables) => { 184 | const id = variables.id as string; 185 | const client = new Graphlit(); 186 | 187 | try { 188 | const response = await client.getCollection(id); 189 | return { 190 | contents: [ 191 | { 192 | uri: uri.toString(), 193 | text: JSON.stringify( 194 | { 195 | id: response.collection?.id, 196 | name: response.collection?.name, 197 | contents: (response.collection?.contents || []) 198 | .filter((content) => content !== null) 199 | .map((content) => `contents://${content.id}`), 200 | }, 201 | null, 202 | 2 203 | ), 204 | mimeType: "application/json", 205 | }, 206 | ], 207 | }; 208 | } catch (error) { 209 | console.error("Error fetching collection:", error); 210 | return { 211 | contents: [], 212 | }; 213 | } 214 | } 215 | ); 216 | 217 | server.resource( 218 | "Contents list: Returns list of content resources.", 219 | new ResourceTemplate("contents://", { 220 | list: async (extra) => { 221 | const client = new Graphlit(); 222 | 223 | const filter: ContentFilter = { 224 | states: [EntityState.Finished], // filter on finished contents only 225 | }; 226 | 227 | try { 228 | const response = await client.queryContents(filter); 229 | 230 | return { 231 | resources: (response.contents?.results || []) 232 | .filter((content) => content !== null) 233 | .map((content) => ({ 234 | name: content.name, 235 | description: content.description || "", 236 | uri: `contents://${content.id}`, 237 | mimeType: content.mimeType || "text/markdown", 238 | })), 239 | }; 240 | } catch (error) { 241 | console.error("Error fetching content list:", error); 242 | return { resources: [] }; 243 | } 244 | }, 245 | }), 246 | async (uri, variables) => { 247 | return { 248 | contents: [], 249 | }; 250 | } 251 | ); 252 | 253 | server.resource( 254 | "Content: Returns content metadata and complete Markdown text. Accepts content resource URI, i.e. contents://{id}, where 'id' is a content identifier.", 255 | new ResourceTemplate("contents://{id}", { list: undefined }), 256 | async (uri: URL, variables) => { 257 | const id = variables.id as string; 258 | const client = new Graphlit(); 259 | 260 | try { 261 | const response = await client.getContent(id); 262 | 263 | const content = response.content; 264 | 265 | return { 266 | contents: [ 267 | { 268 | uri: uri.toString(), 269 | text: formatContent(response), 270 | mimeType: "text/markdown", 271 | }, 272 | ], 273 | }; 274 | } catch (error) { 275 | console.error("Error fetching content:", error); 276 | return { 277 | contents: [], 278 | }; 279 | } 280 | } 281 | ); 282 | 283 | server.resource( 284 | "Workflows: Returns list of workflow resources.", 285 | new ResourceTemplate("workflows://", { 286 | list: async (extra) => { 287 | const client = new Graphlit(); 288 | 289 | try { 290 | const response = await client.queryWorkflows(); 291 | 292 | return { 293 | resources: (response.workflows?.results || []) 294 | .filter((workflow) => workflow !== null) 295 | .map((workflow) => ({ 296 | name: workflow.name, 297 | uri: `workflows://${workflow.id}`, 298 | })), 299 | }; 300 | } catch (error) { 301 | console.error("Error fetching workflow list:", error); 302 | return { resources: [] }; 303 | } 304 | }, 305 | }), 306 | async (uri, variables) => { 307 | return { 308 | contents: [], 309 | }; 310 | } 311 | ); 312 | 313 | server.resource( 314 | "Workflow: Returns workflow metadata. Accepts workflow resource URI, i.e. workflows://{id}, where 'id' is a workflow identifier.", 315 | new ResourceTemplate("workflows://{id}", { list: undefined }), 316 | async (uri: URL, variables) => { 317 | const id = variables.id as string; 318 | const client = new Graphlit(); 319 | 320 | try { 321 | const response = await client.getWorkflow(id); 322 | return { 323 | contents: [ 324 | { 325 | uri: uri.toString(), 326 | text: JSON.stringify(response.workflow, null, 2), 327 | mimeType: "application/json", 328 | }, 329 | ], 330 | }; 331 | } catch (error) { 332 | console.error("Error fetching workflow:", error); 333 | return { 334 | contents: [], 335 | }; 336 | } 337 | } 338 | ); 339 | 340 | server.resource( 341 | "Specifications: Returns list of specification resources.", 342 | new ResourceTemplate("specifications://", { 343 | list: async (extra) => { 344 | const client = new Graphlit(); 345 | 346 | try { 347 | const response = await client.querySpecifications(); 348 | 349 | return { 350 | resources: (response.specifications?.results || []) 351 | .filter((specification) => specification !== null) 352 | .map((specification) => ({ 353 | name: specification.name, 354 | uri: `specifications://${specification.id}`, 355 | })), 356 | }; 357 | } catch (error) { 358 | console.error("Error fetching specification list:", error); 359 | return { resources: [] }; 360 | } 361 | }, 362 | }), 363 | async (uri, variables) => { 364 | return { 365 | contents: [], 366 | }; 367 | } 368 | ); 369 | 370 | server.resource( 371 | "Specification: Returns specification metadata. Accepts specification resource URI, i.e. specifications://{id}, where 'id' is a specification identifier.", 372 | new ResourceTemplate("specifications://{id}", { list: undefined }), 373 | async (uri: URL, variables) => { 374 | const id = variables.id as string; 375 | const client = new Graphlit(); 376 | 377 | try { 378 | const response = await client.getSpecification(id); 379 | return { 380 | contents: [ 381 | { 382 | uri: uri.toString(), 383 | text: JSON.stringify(response.specification, null, 2), 384 | mimeType: "application/json", 385 | }, 386 | ], 387 | }; 388 | } catch (error) { 389 | console.error("Error fetching specification:", error); 390 | return { 391 | contents: [], 392 | }; 393 | } 394 | } 395 | ); 396 | 397 | server.resource( 398 | "Project: Returns current Graphlit project metadata including credits and LLM tokens used in the last day, available quota, and default content workflow. Accepts project resource URI, i.e. projects://{id}, where 'id' is a project identifier.", 399 | new ResourceTemplate("projects://", { list: undefined }), 400 | async (uri: URL, variables) => { 401 | const id = variables.id as string; 402 | const client = new Graphlit(); 403 | 404 | try { 405 | const startDate = new Date(Date.now() - 24 * 60 * 60 * 1000); // 1 day ago 406 | const duration = "P1D"; // ISO duration for 1 day 407 | 408 | const cresponse = await client.queryProjectCredits( 409 | startDate.toISOString(), 410 | duration 411 | ); 412 | const credits = cresponse?.credits; 413 | 414 | const tresponse = await client.queryProjectTokens( 415 | startDate.toISOString(), 416 | duration 417 | ); 418 | const tokens = tresponse?.tokens; 419 | 420 | const response = await client.getProject(); 421 | 422 | return { 423 | contents: [ 424 | { 425 | uri: uri.toString(), 426 | text: JSON.stringify( 427 | { 428 | name: response.project?.name, 429 | workflow: response.project?.workflow, 430 | quota: response.project?.quota, 431 | credits: credits, 432 | tokens: tokens, 433 | }, 434 | null, 435 | 2 436 | ), 437 | mimeType: "application/json", 438 | }, 439 | ], 440 | }; 441 | } catch (error) { 442 | console.error("Error fetching project:", error); 443 | return { 444 | contents: [], 445 | }; 446 | } 447 | } 448 | ); 449 | } 450 | 451 | function formatConversation(response: GetConversationQuery): string { 452 | const results: string[] = []; 453 | 454 | const conversation = response.conversation; 455 | 456 | if (!conversation) { 457 | return ""; 458 | } 459 | 460 | // Basic conversation details 461 | results.push(`**Conversation ID:** ${conversation.id}`); 462 | 463 | // Messages 464 | if (conversation.messages?.length) { 465 | conversation.messages.forEach((message) => { 466 | results.push(`${message?.role}:\n${message?.message}` || ""); 467 | 468 | if (message?.citations?.length) { 469 | message.citations.forEach((citation) => { 470 | results.push( 471 | `**Cited Source [${citation?.index}]**: contents://${citation?.content?.id}` 472 | ); 473 | results.push(`**Cited Text**:\n${citation?.text || ""}`); 474 | }); 475 | } 476 | 477 | results.push("\n---\n"); 478 | }); 479 | } 480 | 481 | return results.join("\n"); 482 | } 483 | 484 | function formatContent(response: GetContentQuery): string { 485 | const results: string[] = []; 486 | 487 | const content = response.content; 488 | 489 | if (!content) { 490 | return ""; 491 | } 492 | 493 | // Basic content details 494 | results.push(`**Content ID:** ${content.id}`); 495 | 496 | if (content.type === ContentTypes.File) { 497 | results.push(`**File Type:** [${content.fileType}]`); 498 | results.push(`**File Name:** ${content.fileName}`); 499 | } else { 500 | results.push(`**Type:** [${content.type}]`); 501 | if ( 502 | content.type !== ContentTypes.Page && 503 | content.type !== ContentTypes.Email 504 | ) { 505 | results.push(`**Name:** ${content.name}`); 506 | } 507 | } 508 | 509 | // Optional metadata 510 | 511 | // 512 | // REVIEW: Not sure if source URI is useful for MCP client 513 | // 514 | //if (content.uri) results.push(`**URI:** ${content.uri}`); 515 | 516 | if (content.masterUri) 517 | results.push(`**Downloadable Original:** ${content.masterUri}`); 518 | if (content.imageUri) 519 | results.push(`**Downloadable Image:** ${content.imageUri}`); 520 | if (content.audioUri) 521 | results.push(`**Downloadable Audio:** ${content.audioUri}`); 522 | 523 | if (content.creationDate) 524 | results.push(`**Ingestion Date:** ${content.creationDate}`); 525 | if (content.originalDate) 526 | results.push(`**Author Date:** ${content.originalDate}`); 527 | 528 | // Issue details 529 | if (content.issue) { 530 | const issue = content.issue; 531 | const issueAttributes = [ 532 | ["Title", issue.title], 533 | ["Identifier", issue.identifier], 534 | ["Type", issue.type], 535 | ["Project", issue.project], 536 | ["Team", issue.team], 537 | ["Status", issue.status], 538 | ["Priority", issue.priority], 539 | ]; 540 | results.push( 541 | ...issueAttributes 542 | .filter(([_, value]) => value) 543 | .map(([label, value]) => `**${label}:** ${value}`) 544 | ); 545 | 546 | if (issue.labels?.length) { 547 | results.push(`**Labels:** ${issue.labels.join(", ")}`); 548 | } 549 | } 550 | 551 | // Email details 552 | if (content.email) { 553 | const email = content.email; 554 | const formatRecipient = (r: any) => `${r.name} <${r.email}>`; 555 | 556 | const emailAttributes = [ 557 | ["Subject", email.subject], 558 | ["Sensitivity", email.sensitivity], 559 | ["Priority", email.priority], 560 | ["Importance", email.importance], 561 | ["Labels", email.labels?.join(", ")], 562 | ["To", email.to?.map(formatRecipient).join(", ")], 563 | ["From", email.from?.map(formatRecipient).join(", ")], 564 | ["CC", email.cc?.map(formatRecipient).join(", ")], 565 | ["BCC", email.bcc?.map(formatRecipient).join(", ")], 566 | ]; 567 | results.push( 568 | ...emailAttributes 569 | .filter(([_, value]) => value) 570 | .map(([label, value]) => `**${label}:** ${value}`) 571 | ); 572 | } 573 | 574 | // Document details 575 | if (content.document) { 576 | const doc = content.document; 577 | if (doc.title) results.push(`**Title:** ${doc.title}`); 578 | if (doc.author) results.push(`**Author:** ${doc.author}`); 579 | } 580 | 581 | // Audio details 582 | if (content.audio) { 583 | const audio = content.audio; 584 | if (audio.title) results.push(`**Title:** ${audio.title}`); 585 | if (audio.author) results.push(`**Host:** ${audio.author}`); 586 | if (audio.episode) results.push(`**Episode:** ${audio.episode}`); 587 | if (audio.series) results.push(`**Series:** ${audio.series}`); 588 | } 589 | 590 | // Image details 591 | if (content.image) { 592 | const image = content.image; 593 | if (image.description) 594 | results.push(`**Description:** ${image.description}`); 595 | if (image.software) results.push(`**Software:** ${image.software}`); 596 | if (image.make) results.push(`**Make:** ${image.make}`); 597 | if (image.model) results.push(`**Model:** ${image.model}`); 598 | } 599 | 600 | // Collections 601 | if (content.collections) { 602 | results.push( 603 | ...content.collections 604 | .filter((collection) => collection !== null) 605 | .slice(0, 100) 606 | .map( 607 | (collection) => 608 | `**Collection [${collection.name}]:** collections://${collection.id}` 609 | ) 610 | ); 611 | } 612 | 613 | // Parent Content 614 | if (content.parent) { 615 | results.push(`**Parent Content:** contents://${content.parent.id}`); 616 | } 617 | 618 | // Child Content(s) 619 | if (content.children) { 620 | results.push( 621 | ...content.children 622 | .filter((child) => child !== null) 623 | .slice(0, 100) 624 | .map((child) => `**Child Content:** contents://${child.id}`) 625 | ); 626 | } 627 | 628 | // Links 629 | if (content.links && content.type === ContentTypes.Page) { 630 | results.push( 631 | ...content.links 632 | .slice(0, 1000) 633 | .map((link) => `**${link.linkType} Link:** ${link.uri}`) 634 | ); 635 | } 636 | 637 | // Observations 638 | if (content.observations) { 639 | results.push( 640 | ...content.observations 641 | .filter((observation) => observation !== null) 642 | .filter((observation) => observation.observable !== null) 643 | .slice(0, 100) 644 | .map( 645 | (observation) => 646 | `**${observation.type}:** ${observation.type.toLowerCase()}s://${observation.observable.id}` 647 | ) 648 | ); 649 | } 650 | 651 | // Content 652 | if (content.pages?.length) { 653 | content.pages.forEach((page) => { 654 | if (page.chunks?.length) { 655 | results.push(`**Page #${(page.index || 0) + 1}:**`); 656 | results.push( 657 | ...(page.chunks 658 | ?.filter((chunk) => chunk?.text) 659 | .map((chunk) => chunk?.text || "") || []) 660 | ); 661 | results.push("\n---\n"); 662 | } 663 | }); 664 | } 665 | 666 | if (content.segments?.length) { 667 | content.segments.forEach((segment) => { 668 | results.push( 669 | `**Transcript Segment [${segment.startTime}-${segment.endTime}]:**` 670 | ); 671 | results.push(segment.text || ""); 672 | results.push("\n---\n"); 673 | }); 674 | } 675 | 676 | if (content.frames?.length) { 677 | content.frames.forEach((frame) => { 678 | results.push(`**Frame #${(frame.index || 0) + 1}:**`); 679 | results.push(frame.text || ""); 680 | results.push("\n---\n"); 681 | }); 682 | } 683 | 684 | if ( 685 | !content.pages?.length && 686 | !content.segments?.length && 687 | !content.frames?.length && 688 | content.markdown 689 | ) { 690 | results.push(content.markdown); 691 | results.push("\n"); 692 | } 693 | 694 | return results.join("\n"); 695 | } 696 | -------------------------------------------------------------------------------- /src/tools.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import mime from "mime-types"; 4 | import { Graphlit, Types } from "graphlit-client"; 5 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 6 | import { z } from "zod"; 7 | import { 8 | ConversationFilter, 9 | CollectionFilter, 10 | ContentFilter, 11 | ContentTypes, 12 | FeedFilter, 13 | FeedServiceTypes, 14 | EmailListingTypes, 15 | SearchServiceTypes, 16 | FeedListingTypes, 17 | FeedTypes, 18 | NotionTypes, 19 | RerankingModelServiceTypes, 20 | RetrievalStrategyTypes, 21 | GoogleDriveAuthenticationTypes, 22 | SharePointAuthenticationTypes, 23 | FileTypes, 24 | TextTypes, 25 | SearchTypes, 26 | ContentPublishingServiceTypes, 27 | ContentPublishingFormats, 28 | ElevenLabsModels, 29 | IntegrationServiceTypes, 30 | TwitterListingTypes, 31 | ConversationSearchTypes, 32 | PromptStrategyTypes, 33 | OpenAiImageModels, 34 | TimedPolicyRecurrenceTypes, 35 | } from "graphlit-client/dist/generated/graphql-types.js"; 36 | 37 | export function registerTools(server: McpServer) { 38 | // Default 15-minute recurrence schedule policy for feeds 39 | const schedulePolicy = { 40 | recurrenceType: TimedPolicyRecurrenceTypes.Repeat, 41 | repeatInterval: "PT15M", 42 | }; 43 | server.tool( 44 | "configureProject", 45 | `Configures the default content workflow and conversation specification for the Graphlit project. 46 | Only needed if user asks to configure the project defaults. *Do not* call unless specifically asked for by the user. 47 | To reset the project configuration to 'factory state', assign False or null to all parameters. 48 | Optionally accepts whether to configure the default specification for LLM conversations. Defaults to using OpenAI GPT-4o, if not assigned. 49 | Optionally accepts whether to enable high-quality document and web page preparation using a vision LLM. Defaults to using Azure AI Document Intelligence for document preparation, if not assigned. 50 | Optionally accepts whether to enable entity extraction using LLM into the knowledge graph. Defaults to no entity extraction, if not assigned. 51 | Optionally accepts the preferred model provider service type, i.e. Anthropic, OpenAI, Google. Defaults to Anthropic if not provided. 52 | Returns the project identifier.`, 53 | { 54 | modelServiceType: z 55 | .nativeEnum(Types.ModelServiceTypes) 56 | .optional() 57 | .default(Types.ModelServiceTypes.Anthropic) 58 | .describe( 59 | "Preferred model provider service type for all specifications, i.e. Anthropic, OpenAI, Google. Defaults to Anthropic if not provided." 60 | ), 61 | configureConversationSpecification: z 62 | .boolean() 63 | .optional() 64 | .default(false) 65 | .describe( 66 | "Whether to configure the default specification for LLM conversations. Defaults to False." 67 | ), 68 | configurePreparationSpecification: z 69 | .boolean() 70 | .optional() 71 | .default(false) 72 | .describe( 73 | "Whether to configure high-quality document and web page preparation using vision LLM. Defaults to False." 74 | ), 75 | configureExtractionSpecification: z 76 | .boolean() 77 | .optional() 78 | .default(false) 79 | .describe( 80 | "Whether to configure entity extraction using LLM into the knowledge graph. Defaults to False." 81 | ), 82 | }, 83 | async ({ 84 | modelServiceType, 85 | configureConversationSpecification, 86 | configurePreparationSpecification, 87 | configureExtractionSpecification, 88 | }) => { 89 | const client = new Graphlit(); 90 | 91 | var preparationSpecificationId; 92 | var extractionSpecificationId; 93 | var completionSpecificationId; 94 | var workflowId; 95 | 96 | switch (modelServiceType) { 97 | case Types.ModelServiceTypes.Anthropic: 98 | case Types.ModelServiceTypes.Google: 99 | case Types.ModelServiceTypes.OpenAi: 100 | break; 101 | default: 102 | throw new Error( 103 | `Unsupported model service type [${modelServiceType}].` 104 | ); 105 | } 106 | 107 | if (configureConversationSpecification) { 108 | var sresponse = await client.upsertSpecification({ 109 | name: "MCP Default Specification: Completion", 110 | type: Types.SpecificationTypes.Completion, 111 | serviceType: modelServiceType, 112 | anthropic: 113 | modelServiceType == Types.ModelServiceTypes.Anthropic 114 | ? { 115 | model: Types.AnthropicModels.Claude_3_7Sonnet, 116 | } 117 | : undefined, 118 | openAI: 119 | modelServiceType == Types.ModelServiceTypes.OpenAi 120 | ? { 121 | model: Types.OpenAiModels.Gpt4OChat_128K, 122 | } 123 | : undefined, 124 | google: 125 | modelServiceType == Types.ModelServiceTypes.Google 126 | ? { 127 | model: Types.GoogleModels.Gemini_2_0Flash, 128 | } 129 | : undefined, 130 | searchType: ConversationSearchTypes.Hybrid, 131 | strategy: { 132 | embedCitations: true, 133 | }, 134 | promptStrategy: { 135 | type: PromptStrategyTypes.OptimizeSearch, // optimize for similarity search 136 | }, 137 | retrievalStrategy: { 138 | type: RetrievalStrategyTypes.Section, // expand chunk to section 139 | }, 140 | rerankingStrategy: { 141 | serviceType: RerankingModelServiceTypes.Cohere, 142 | }, 143 | }); 144 | 145 | completionSpecificationId = sresponse.upsertSpecification?.id; 146 | } 147 | 148 | if (configurePreparationSpecification) { 149 | var sresponse = await client.upsertSpecification({ 150 | name: "MCP Default Specification: Preparation", 151 | type: Types.SpecificationTypes.Preparation, 152 | serviceType: modelServiceType, 153 | anthropic: 154 | modelServiceType == Types.ModelServiceTypes.Anthropic 155 | ? { 156 | model: Types.AnthropicModels.Claude_3_7Sonnet, 157 | enableThinking: true, 158 | } 159 | : undefined, 160 | openAI: 161 | modelServiceType == Types.ModelServiceTypes.OpenAi 162 | ? { 163 | model: Types.OpenAiModels.Gpt4O_128K, 164 | } 165 | : undefined, 166 | google: 167 | modelServiceType == Types.ModelServiceTypes.Google 168 | ? { 169 | model: Types.GoogleModels.Gemini_2_5ProPreview, 170 | } 171 | : undefined, 172 | }); 173 | 174 | preparationSpecificationId = sresponse.upsertSpecification?.id; 175 | } 176 | 177 | if (configureExtractionSpecification) { 178 | var sresponse = await client.upsertSpecification({ 179 | name: "MCP Default Specification: Extraction", 180 | type: Types.SpecificationTypes.Extraction, 181 | serviceType: modelServiceType, 182 | anthropic: 183 | modelServiceType == Types.ModelServiceTypes.Anthropic 184 | ? { 185 | model: Types.AnthropicModels.Claude_3_7Sonnet, 186 | } 187 | : undefined, 188 | openAI: 189 | modelServiceType == Types.ModelServiceTypes.OpenAi 190 | ? { 191 | model: Types.OpenAiModels.Gpt4O_128K, 192 | } 193 | : undefined, 194 | google: 195 | modelServiceType == Types.ModelServiceTypes.Google 196 | ? { 197 | model: Types.GoogleModels.Gemini_2_0Flash, 198 | } 199 | : undefined, 200 | }); 201 | 202 | extractionSpecificationId = sresponse.upsertSpecification?.id; 203 | } 204 | 205 | const wresponse = await client.upsertWorkflow({ 206 | name: "MCP Default Workflow", 207 | preparation: 208 | preparationSpecificationId !== undefined 209 | ? { 210 | jobs: [ 211 | { 212 | connector: { 213 | type: Types.FilePreparationServiceTypes.ModelDocument, 214 | modelDocument: { 215 | specification: { id: preparationSpecificationId }, 216 | }, 217 | }, 218 | }, 219 | ], 220 | } 221 | : undefined, 222 | extraction: 223 | extractionSpecificationId !== undefined 224 | ? { 225 | jobs: [ 226 | { 227 | connector: { 228 | type: Types.EntityExtractionServiceTypes.ModelText, 229 | modelText: { 230 | specification: { id: extractionSpecificationId }, 231 | }, 232 | }, 233 | }, 234 | { 235 | connector: { 236 | type: Types.EntityExtractionServiceTypes.ModelImage, 237 | modelImage: { 238 | specification: { id: extractionSpecificationId }, 239 | }, 240 | }, 241 | }, 242 | ], 243 | } 244 | : undefined, 245 | }); 246 | 247 | workflowId = wresponse.upsertWorkflow?.id; 248 | 249 | try { 250 | const response = await client.updateProject({ 251 | specification: 252 | completionSpecificationId !== undefined 253 | ? { id: completionSpecificationId } 254 | : undefined, 255 | workflow: workflowId !== undefined ? { id: workflowId } : undefined, 256 | }); 257 | 258 | return { 259 | content: [ 260 | { 261 | type: "text", 262 | text: JSON.stringify({ id: response.updateProject?.id }, null, 2), 263 | }, 264 | ], 265 | }; 266 | } catch (err: unknown) { 267 | const error = err as Error; 268 | return { 269 | content: [ 270 | { 271 | type: "text", 272 | text: `Error: ${error.message}`, 273 | }, 274 | ], 275 | isError: true, 276 | }; 277 | } 278 | } 279 | ); 280 | 281 | // Simple ISO duration parser 282 | function parseDuration(durationStr: string): number { 283 | const match = durationStr.match( 284 | /^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/ 285 | ); 286 | 287 | if (!match) { 288 | throw new Error(`Invalid ISO 8601 duration: ${durationStr}`); 289 | } 290 | 291 | const [, days, hours, minutes, seconds] = match.map(Number); 292 | 293 | const totalMs = 294 | (days || 0) * 24 * 60 * 60 * 1000 + 295 | (hours || 0) * 60 * 60 * 1000 + 296 | (minutes || 0) * 60 * 1000 + 297 | (seconds || 0) * 1000; 298 | 299 | return totalMs; 300 | } 301 | 302 | server.tool( 303 | "queryProjectUsage", 304 | `Queries project usage records. 305 | Usage record name describes the operation, i.e. 'Prompt completion', 'Text embedding', 'GraphQL', 'Entity Event'. 306 | 'GraphQL' usage records are used for GraphQL operations, i.e. 'queryContents', 'retrieveSources', 'askGraphlit', etc. 307 | 'Entity Event' usage records are used for async compute operations. 308 | 'Text embedding' usage records are used for text embedding operations. 309 | 'Prompt completion' usage records are used for LLM prompt completion operations, i.e. when using 'promptConversation'. 310 | 'Data extraction' usage records are used for data extraction operations, using LLMs to extract knowledge graph entities. 311 | Look at 'metric' field for the type of metric captured in the usage record, i.e. BYTES, TOKENS, UNITS, REQUESTS. 312 | Look for 'credits' field which describes how many credits were charged by the operation. 313 | Look for 'promptTokens', 'completionTokens' and (total) 'tokens' fields which describe the number of tokens used by the operation. 314 | Look for 'request', 'response' and 'variables' fields which describe the GraphQL operation. 315 | Look for 'count' for the number of units used by the operation, for example, number of pages processed by document preparation. 316 | Accepts an optional recency filter for usage records 'in last' timespan. 317 | Returns a list of usage records, which describe the billable audit log of all Graphlit API operations.`, 318 | { 319 | inLast: z 320 | .string() 321 | .optional() 322 | .default("PT1H") 323 | .describe( 324 | "Recency filter for usage records 'in last' timespan, optional. Defaults to PT1H. Should be ISO 8601 format, for example, 'PT1H' for last hour, 'P1D' for last day, 'P7D' for last week, 'P30D' for last month. Doesn't support weeks or months explicitly." 325 | ), 326 | }, 327 | async ({ inLast }) => { 328 | const client = new Graphlit(); 329 | 330 | try { 331 | const durationMs = parseDuration(inLast); 332 | const startDate = new Date(Date.now() - durationMs); 333 | 334 | let offset = 0; 335 | const limit = 1000; 336 | const usage: any[] = []; 337 | 338 | while (true) { 339 | const response = await client.queryProjectUsage( 340 | startDate, 341 | inLast, 342 | [], 343 | [], 344 | offset, 345 | limit 346 | ); 347 | const usageBatch = response.usage ?? []; 348 | 349 | for (const record of usageBatch) { 350 | if (record) { 351 | const remappedRecord: any = { 352 | date: record.date, 353 | name: record.name, 354 | metric: record.metric, 355 | credits: record.credits, 356 | count: record.count, 357 | duration: record.duration, 358 | entityType: record.entityType, 359 | entityId: record.entityId, 360 | ownerId: record.ownerId, 361 | workflow: record.workflow, 362 | contentType: record.contentType, 363 | fileType: record.fileType, 364 | uri: record.uri, 365 | modelService: record.modelService, 366 | modelName: record.modelName, 367 | //prompt: record.prompt, 368 | promptTokens: record.promptTokens, 369 | //completion: record.completion, 370 | completionTokens: record.completionTokens, 371 | tokens: record.tokens, 372 | operation: record.operation, 373 | }; 374 | 375 | // Remove any fields that are "", null, or undefined 376 | for (const key of Object.keys(remappedRecord)) { 377 | const value = remappedRecord[key]; 378 | if (value === "" || value === null || value === undefined) { 379 | delete remappedRecord[key]; 380 | } 381 | } 382 | 383 | usage.push(remappedRecord); 384 | } 385 | } 386 | 387 | if (usageBatch.length < limit) { 388 | // No more pages 389 | break; 390 | } 391 | 392 | offset += limit; 393 | } 394 | 395 | return { 396 | content: [ 397 | { 398 | type: "text", 399 | text: JSON.stringify(usage, null, 2), 400 | }, 401 | ], 402 | }; 403 | } catch (err: unknown) { 404 | const error = err as Error; 405 | return { 406 | content: [ 407 | { 408 | type: "text", 409 | text: `Error: ${error.message}`, 410 | }, 411 | ], 412 | isError: true, 413 | }; 414 | } 415 | } 416 | ); 417 | 418 | server.tool( 419 | "askGraphlit", 420 | `Ask questions about using the Graphlit Platform, or specifically about the Graphlit API or SDKs. 421 | When the user asks about how to use the Graphlit API or SDKs, use this tool to provide a code sample in Python, TypeScript or C#. 422 | Accepts an LLM user prompt. 423 | Returns the LLM prompt completion in Markdown format.`, 424 | { 425 | prompt: z.string().describe("LLM user prompt."), 426 | }, 427 | async ({ prompt }) => { 428 | const client = new Graphlit(); 429 | 430 | try { 431 | const response = await client.askGraphlit(prompt); 432 | 433 | const message = response.askGraphlit?.message; 434 | 435 | return { 436 | content: [ 437 | { 438 | type: "text", 439 | text: JSON.stringify(message, null, 2), 440 | }, 441 | ], 442 | }; 443 | } catch (err: unknown) { 444 | const error = err as Error; 445 | return { 446 | content: [ 447 | { 448 | type: "text", 449 | text: `Error: ${error.message}`, 450 | }, 451 | ], 452 | isError: true, 453 | }; 454 | } 455 | } 456 | ); 457 | 458 | server.tool( 459 | "promptConversation", 460 | `Prompts an LLM conversation about your entire Graphlit knowledge base. 461 | Uses hybrid vector search based on user prompt for locating relevant content sources. Uses LLM to complete the user prompt with the configured LLM. 462 | Maintains conversation history between 'user' and LLM 'assistant'. 463 | Prefer 'promptConversation' when the user intends to start or continue an ongoing conversation about the entire Graphlit knowledge base. 464 | Similar to 'retrieveSources' but does not perform content metadata filtering. 465 | Accepts an LLM user prompt and optional conversation identifier. Will either create a new conversation or continue an existing one. 466 | Will use the default specification for LLM conversations, which is optionally configured with the 'configureProject' tool. 467 | Returns the conversation identifier, completed LLM message, and any citations from the LLM response.`, 468 | { 469 | prompt: z.string().describe("User prompt."), 470 | conversationId: z 471 | .string() 472 | .optional() 473 | .describe("Conversation identifier, optional."), 474 | }, 475 | async ({ prompt, conversationId }) => { 476 | const client = new Graphlit(); 477 | 478 | try { 479 | const response = await client.promptConversation( 480 | prompt, 481 | conversationId 482 | ); 483 | 484 | return { 485 | content: [ 486 | { 487 | type: "text", 488 | text: JSON.stringify( 489 | { 490 | id: response.promptConversation?.conversation?.id, 491 | message: response.promptConversation?.message?.message, 492 | citations: response.promptConversation?.message?.citations, 493 | }, 494 | null, 495 | 2 496 | ), 497 | }, 498 | ], 499 | }; 500 | } catch (err: unknown) { 501 | const error = err as Error; 502 | return { 503 | content: [ 504 | { 505 | type: "text", 506 | text: `Error: ${error.message}`, 507 | }, 508 | ], 509 | isError: true, 510 | }; 511 | } 512 | } 513 | ); 514 | 515 | server.tool( 516 | "retrieveSources", 517 | `Retrieve relevant content sources from Graphlit knowledge base. Do *not* use for retrieving content by content identifier - retrieve content resource instead, with URI 'contents://{id}'. 518 | Accepts an LLM user prompt for content retrieval. For best retrieval quality, provide only key words or phrases from the user prompt, which will be used to create text embeddings for a vector search query. 519 | Only use when there is a valid LLM user prompt for content retrieval, otherwise use 'queryContents'. For example 'recent content' is not a useful user prompt, since it doesn't reference the text in the content. 520 | Only use for 'one shot' retrieval of content sources, i.e. when the user is not interested in having a conversation about the content. 521 | Accepts an optional ingestion recency filter (defaults to null, meaning all time), and optional content type and file type filters. 522 | Also accepts optional feed and collection identifiers to filter content by. 523 | Returns the ranked content sources, including their content resource URI to retrieve the complete Markdown text.`, 524 | { 525 | prompt: z.string().describe("LLM user prompt for content retrieval."), 526 | inLast: z 527 | .string() 528 | .optional() 529 | .describe( 530 | "Recency filter for content ingested 'in last' timespan, optional. Should be ISO 8601 format, for example, 'PT1H' for last hour, 'P1D' for last day, 'P7D' for last week, 'P30D' for last month. Doesn't support weeks or months explicitly." 531 | ), 532 | type: z 533 | .nativeEnum(ContentTypes) 534 | .optional() 535 | .describe( 536 | "Content type filter, optional. One of: Email, Event, File, Issue, Message, Page, Post, Text." 537 | ), 538 | fileType: z 539 | .nativeEnum(FileTypes) 540 | .optional() 541 | .describe( 542 | "File type filter, optional. One of: Animation, Audio, Code, Data, Document, Drawing, Email, Geometry, Image, Package, PointCloud, Shape, Video." 543 | ), 544 | feeds: z 545 | .array(z.string()) 546 | .optional() 547 | .describe("Feed identifiers to filter content by, optional."), 548 | collections: z 549 | .array(z.string()) 550 | .optional() 551 | .describe("Collection identifiers to filter content by, optional."), 552 | }, 553 | async ({ prompt, type, fileType, inLast, feeds, collections }) => { 554 | const client = new Graphlit(); 555 | 556 | try { 557 | const filter: ContentFilter = { 558 | searchType: SearchTypes.Hybrid, 559 | feeds: feeds?.map((feed) => ({ id: feed })), 560 | collections: collections?.map((collection) => ({ id: collection })), 561 | createdInLast: inLast, 562 | types: type ? [type] : null, 563 | fileTypes: fileType ? [fileType] : null, 564 | }; 565 | 566 | const response = await client.retrieveSources( 567 | prompt, 568 | filter, 569 | undefined, 570 | { 571 | type: RetrievalStrategyTypes.Section, 572 | contentLimit: 50, // number of content sources to retrieve prior to reranking 573 | disableFallback: true, // disable fallback to recent contents 574 | }, 575 | { 576 | serviceType: RerankingModelServiceTypes.Cohere, 577 | } 578 | ); 579 | 580 | const sources = response.retrieveSources?.results || []; 581 | 582 | return { 583 | content: sources 584 | .filter((source) => source !== null) 585 | .map((source) => ({ 586 | type: "text", 587 | mimeType: "application/json", 588 | text: JSON.stringify( 589 | { 590 | id: source.content?.id, 591 | relevance: source.relevance, 592 | resourceUri: `contents://${source.content?.id}`, 593 | text: source.text, 594 | mimeType: "text/markdown", 595 | }, 596 | null, 597 | 2 598 | ), 599 | })), 600 | }; 601 | } catch (err: unknown) { 602 | const error = err as Error; 603 | return { 604 | content: [ 605 | { 606 | type: "text", 607 | text: `Error: ${error.message}`, 608 | }, 609 | ], 610 | isError: true, 611 | }; 612 | } 613 | } 614 | ); 615 | 616 | const PointFilter = z.object({ 617 | latitude: z 618 | .number() 619 | .min(-90) 620 | .max(90) 621 | .describe("The latitude, must be between -90 and 90."), 622 | longitude: z 623 | .number() 624 | .min(-180) 625 | .max(180) 626 | .describe("The longitude, must be between -180 and 180."), 627 | distance: z 628 | .number() 629 | .optional() 630 | .describe("The distance radius (in meters)."), 631 | }); 632 | 633 | // 634 | // REVIEW: MCP clients don't handle Base64-encoded data very well, 635 | // will often exceed the LLM context window to return from the tool 636 | // so, we only can support similar images by URL 637 | server.tool( 638 | "retrieveImages", 639 | `Retrieve images from Graphlit knowledge base. Provides image-specific retrieval when image similarity search is desired. 640 | Do *not* use for retrieving content by content identifier - retrieve content resource instead, with URI 'contents://{id}'. 641 | Accepts image URL. Image will be used for similarity search using image embeddings. 642 | Accepts optional geo-location filter for search by latitude, longitude and optional distance radius. Images taken with GPS enabled are searchable by geo-location. 643 | Also accepts optional recency filter (defaults to null, meaning all time), and optional feed and collection identifiers to filter images by. 644 | Returns the matching images, including their content resource URI to retrieve the complete Markdown text.`, 645 | { 646 | url: z 647 | .string() 648 | .describe( 649 | "URL of image which will be used for similarity search using image embeddings." 650 | ), 651 | inLast: z 652 | .string() 653 | .optional() 654 | .describe( 655 | "Recency filter for images ingested 'in last' timespan, optional. Should be ISO 8601 format, for example, 'PT1H' for last hour, 'P1D' for last day, 'P7D' for last week, 'P30D' for last month. Doesn't support weeks or months explicitly." 656 | ), 657 | feeds: z 658 | .array(z.string()) 659 | .optional() 660 | .describe("Feed identifiers to filter images by, optional."), 661 | collections: z 662 | .array(z.string()) 663 | .optional() 664 | .describe("Collection identifiers to filter images by, optional."), 665 | location: PointFilter.optional().describe( 666 | "Geo-location filter for search by latitude, longitude and optional distance radius." 667 | ), 668 | limit: z 669 | .number() 670 | .optional() 671 | .default(100) 672 | .describe( 673 | "Limit the number of images to be returned. Defaults to 100." 674 | ), 675 | }, 676 | async ({ url, inLast, feeds, collections, location, limit }) => { 677 | const client = new Graphlit(); 678 | 679 | var data; 680 | var mimeType; 681 | 682 | if (url) { 683 | const fetchResponse = await fetch(url); 684 | if (!fetchResponse.ok) { 685 | throw new Error( 686 | `Failed to fetch data from ${url}: ${fetchResponse.statusText}` 687 | ); 688 | } 689 | const arrayBuffer = await fetchResponse.arrayBuffer(); 690 | const buffer = Buffer.from(arrayBuffer); 691 | 692 | data = buffer.toString("base64"); 693 | mimeType = 694 | fetchResponse.headers.get("content-type") || 695 | "application/octet-stream"; 696 | } 697 | 698 | try { 699 | const filter: ContentFilter = { 700 | imageData: data, 701 | imageMimeType: mimeType, 702 | searchType: SearchTypes.Vector, 703 | feeds: feeds?.map((feed) => ({ id: feed })), 704 | collections: collections?.map((collection) => ({ id: collection })), 705 | location: location, 706 | createdInLast: inLast, 707 | types: [ContentTypes.File], 708 | fileTypes: [FileTypes.Image], 709 | limit: limit, 710 | }; 711 | const response = await client.queryContents(filter); 712 | 713 | const contents = response.contents?.results || []; 714 | 715 | return { 716 | content: contents 717 | .filter((content) => content !== null) 718 | .map((content) => ({ 719 | type: "text", 720 | mimeType: "application/json", 721 | text: JSON.stringify( 722 | { 723 | id: content.id, 724 | relevance: content.relevance, 725 | fileName: content.fileName, 726 | resourceUri: `contents://${content.id}`, 727 | uri: content.imageUri, 728 | mimeType: content.mimeType, 729 | }, 730 | null, 731 | 2 732 | ), 733 | })), 734 | }; 735 | } catch (err: unknown) { 736 | const error = err as Error; 737 | return { 738 | content: [ 739 | { 740 | type: "text", 741 | text: `Error: ${error.message}`, 742 | }, 743 | ], 744 | isError: true, 745 | }; 746 | } 747 | } 748 | ); 749 | 750 | server.tool( 751 | "extractText", 752 | `Extracts JSON data from text using LLM. 753 | Accepts text to be extracted, and JSON schema which describes the data which will be extracted. JSON schema needs be of type 'object' and include 'properties' and 'required' fields. 754 | Optionally accepts text prompt which is provided to LLM to guide data extraction. Defaults to 'Extract data using the tools provided'. 755 | Returns extracted JSON from text.`, 756 | { 757 | text: z.string().describe("Text to be extracted with LLM."), 758 | schema: z 759 | .string() 760 | .describe( 761 | "JSON schema which describes the data which will be extracted. JSON schema needs be of type 'object' and include 'properties' and 'required' fields." 762 | ), 763 | prompt: z 764 | .string() 765 | .optional() 766 | .describe( 767 | "Text prompt which is provided to LLM to guide data extraction, optional." 768 | ), 769 | }, 770 | async ({ text, schema, prompt }) => { 771 | const client = new Graphlit(); 772 | 773 | const DEFAULT_NAME = "extract_json"; 774 | const DEFAULT_PROMPT = ` 775 | Extract data using the tools provided. 776 | `; 777 | 778 | try { 779 | const response = await client.extractText( 780 | prompt || DEFAULT_PROMPT, 781 | text, 782 | [{ name: DEFAULT_NAME, schema: schema }] 783 | ); 784 | 785 | return { 786 | content: [ 787 | { 788 | type: "text", 789 | text: JSON.stringify( 790 | response.extractText 791 | ? response.extractText 792 | .filter((item) => item !== null) 793 | .map((item) => item.value) 794 | : [], 795 | null, 796 | 2 797 | ), 798 | }, 799 | ], 800 | }; 801 | } catch (err: unknown) { 802 | const error = err as Error; 803 | return { 804 | content: [ 805 | { 806 | type: "text", 807 | text: `Error: ${error.message}`, 808 | }, 809 | ], 810 | isError: true, 811 | }; 812 | } 813 | } 814 | ); 815 | 816 | server.tool( 817 | "createCollection", 818 | `Create a collection. 819 | Accepts a collection name, and optional list of content identifiers to add to collection. 820 | Returns the collection identifier`, 821 | { 822 | name: z.string().describe("Collection name."), 823 | contents: z 824 | .array(z.string()) 825 | .optional() 826 | .describe("Content identifiers to add to collection, optional."), 827 | }, 828 | async ({ name, contents }) => { 829 | const client = new Graphlit(); 830 | 831 | try { 832 | const response = await client.createCollection({ 833 | name: name, 834 | contents: contents?.map((content) => ({ id: content })), 835 | }); 836 | 837 | return { 838 | content: [ 839 | { 840 | type: "text", 841 | text: JSON.stringify( 842 | { id: response.createCollection?.id }, 843 | null, 844 | 2 845 | ), 846 | }, 847 | ], 848 | }; 849 | } catch (err: unknown) { 850 | const error = err as Error; 851 | return { 852 | content: [ 853 | { 854 | type: "text", 855 | text: `Error: ${error.message}`, 856 | }, 857 | ], 858 | isError: true, 859 | }; 860 | } 861 | } 862 | ); 863 | 864 | server.tool( 865 | "addContentsToCollection", 866 | `Add contents to a collection. 867 | Accepts a collection identifier and a list of content identifiers to add to collection. 868 | Returns the collection identifier.`, 869 | { 870 | id: z.string().describe("Collection identifier."), 871 | contents: z 872 | .array(z.string()) 873 | .describe("Content identifiers to add to collection."), 874 | }, 875 | async ({ id, contents }) => { 876 | const client = new Graphlit(); 877 | 878 | try { 879 | const response = await client.addContentsToCollections( 880 | contents?.map((content) => ({ id: content })), 881 | [{ id: id }] 882 | ); 883 | 884 | return { 885 | content: [ 886 | { 887 | type: "text", 888 | text: JSON.stringify({ id: id }, null, 2), 889 | }, 890 | ], 891 | }; 892 | } catch (err: unknown) { 893 | const error = err as Error; 894 | return { 895 | content: [ 896 | { 897 | type: "text", 898 | text: `Error: ${error.message}`, 899 | }, 900 | ], 901 | isError: true, 902 | }; 903 | } 904 | } 905 | ); 906 | 907 | server.tool( 908 | "removeContentsFromCollection", 909 | `Remove contents from collection. 910 | Accepts a collection identifier and a list of content identifiers to remove from collection. 911 | Returns the collection identifier.`, 912 | { 913 | id: z.string().describe("Collection identifier."), 914 | contents: z 915 | .array(z.string()) 916 | .describe("Content identifiers to remove from collection."), 917 | }, 918 | async ({ id, contents }) => { 919 | const client = new Graphlit(); 920 | 921 | try { 922 | const response = await client.removeContentsFromCollection( 923 | contents?.map((content) => ({ id: content })), 924 | { id: id } 925 | ); 926 | 927 | return { 928 | content: [ 929 | { 930 | type: "text", 931 | text: JSON.stringify( 932 | { id: response.removeContentsFromCollection?.id }, 933 | null, 934 | 2 935 | ), 936 | }, 937 | ], 938 | }; 939 | } catch (err: unknown) { 940 | const error = err as Error; 941 | return { 942 | content: [ 943 | { 944 | type: "text", 945 | text: `Error: ${error.message}`, 946 | }, 947 | ], 948 | isError: true, 949 | }; 950 | } 951 | } 952 | ); 953 | 954 | server.tool( 955 | "deleteContent", 956 | `Deletes content from Graphlit knowledge base. 957 | Accepts content identifier. 958 | Returns the content identifier and content state, i.e. Deleted.`, 959 | { 960 | id: z.string().describe("Content identifier."), 961 | }, 962 | async ({ id }) => { 963 | const client = new Graphlit(); 964 | 965 | try { 966 | const response = await client.deleteContent(id); 967 | 968 | return { 969 | content: [ 970 | { 971 | type: "text", 972 | text: JSON.stringify(response.deleteContent, null, 2), 973 | }, 974 | ], 975 | }; 976 | } catch (err: unknown) { 977 | const error = err as Error; 978 | return { 979 | content: [ 980 | { 981 | type: "text", 982 | text: `Error: ${error.message}`, 983 | }, 984 | ], 985 | isError: true, 986 | }; 987 | } 988 | } 989 | ); 990 | 991 | server.tool( 992 | "deleteConversation", 993 | `Deletes conversation from Graphlit knowledge base. 994 | Accepts conversation identifier. 995 | Returns the conversation identifier and content state, i.e. Deleted.`, 996 | { 997 | id: z.string().describe("Conversation identifier."), 998 | }, 999 | async ({ id }) => { 1000 | const client = new Graphlit(); 1001 | 1002 | try { 1003 | const response = await client.deleteConversation(id); 1004 | 1005 | return { 1006 | content: [ 1007 | { 1008 | type: "text", 1009 | text: JSON.stringify(response.deleteConversation, null, 2), 1010 | }, 1011 | ], 1012 | }; 1013 | } catch (err: unknown) { 1014 | const error = err as Error; 1015 | return { 1016 | content: [ 1017 | { 1018 | type: "text", 1019 | text: `Error: ${error.message}`, 1020 | }, 1021 | ], 1022 | isError: true, 1023 | }; 1024 | } 1025 | } 1026 | ); 1027 | 1028 | server.tool( 1029 | "deleteCollection", 1030 | `Deletes collection from Graphlit knowledge base. 1031 | Does *not* delete the contents in the collection, only the collection itself. 1032 | Accepts collection identifier. 1033 | Returns the collection identifier and collection state, i.e. Deleted.`, 1034 | { 1035 | id: z.string().describe("Collection identifier."), 1036 | }, 1037 | async ({ id }) => { 1038 | const client = new Graphlit(); 1039 | 1040 | try { 1041 | const response = await client.deleteCollection(id); 1042 | 1043 | return { 1044 | content: [ 1045 | { 1046 | type: "text", 1047 | text: JSON.stringify(response.deleteCollection, null, 2), 1048 | }, 1049 | ], 1050 | }; 1051 | } catch (err: unknown) { 1052 | const error = err as Error; 1053 | return { 1054 | content: [ 1055 | { 1056 | type: "text", 1057 | text: `Error: ${error.message}`, 1058 | }, 1059 | ], 1060 | isError: true, 1061 | }; 1062 | } 1063 | } 1064 | ); 1065 | 1066 | server.tool( 1067 | "deleteFeed", 1068 | `Deletes feed from Graphlit knowledge base. 1069 | *Does* delete the contents in the feed, in addition to the feed itself. 1070 | Accepts feed identifier. 1071 | Returns the feed identifier and feed state, i.e. Deleted.`, 1072 | { 1073 | id: z.string().describe("Feed identifier."), 1074 | }, 1075 | async ({ id }) => { 1076 | const client = new Graphlit(); 1077 | 1078 | try { 1079 | const response = await client.deleteFeed(id); 1080 | 1081 | return { 1082 | content: [ 1083 | { 1084 | type: "text", 1085 | text: JSON.stringify(response.deleteFeed, null, 2), 1086 | }, 1087 | ], 1088 | }; 1089 | } catch (err: unknown) { 1090 | const error = err as Error; 1091 | return { 1092 | content: [ 1093 | { 1094 | type: "text", 1095 | text: `Error: ${error.message}`, 1096 | }, 1097 | ], 1098 | isError: true, 1099 | }; 1100 | } 1101 | } 1102 | ); 1103 | 1104 | server.tool( 1105 | "deleteFeeds", 1106 | `Deletes feeds from Graphlit knowledge base. 1107 | *Does* delete the contents in the feed, in addition to the feed itself. 1108 | Accepts optional feed type filter to limit the feeds which will be deleted. 1109 | Also accepts optional limit of how many feeds to delete, defaults to 100. 1110 | Returns the feed identifiers and feed state, i.e. Deleted.`, 1111 | { 1112 | feedType: z 1113 | .nativeEnum(FeedTypes) 1114 | .optional() 1115 | .describe( 1116 | "Feed type filter, optional. One of: Discord, Email, Intercom, Issue, MicrosoftTeams, Notion, Reddit, Rss, Search, Site, Slack, Web, YouTube, Zendesk." 1117 | ), 1118 | limit: z 1119 | .number() 1120 | .optional() 1121 | .default(100) 1122 | .describe("Limit the number of feeds to be deleted. Defaults to 100."), 1123 | }, 1124 | async ({ feedType, limit }) => { 1125 | const client = new Graphlit(); 1126 | 1127 | try { 1128 | const filter: FeedFilter = { 1129 | types: feedType ? [feedType] : null, 1130 | limit: limit, 1131 | }; 1132 | 1133 | const response = await client.deleteAllFeeds(filter, true); 1134 | 1135 | return { 1136 | content: [ 1137 | { 1138 | type: "text", 1139 | text: JSON.stringify(response.deleteAllFeeds, null, 2), 1140 | }, 1141 | ], 1142 | }; 1143 | } catch (err: unknown) { 1144 | const error = err as Error; 1145 | return { 1146 | content: [ 1147 | { 1148 | type: "text", 1149 | text: `Error: ${error.message}`, 1150 | }, 1151 | ], 1152 | isError: true, 1153 | }; 1154 | } 1155 | } 1156 | ); 1157 | 1158 | server.tool( 1159 | "deleteCollections", 1160 | `Deletes collections from Graphlit knowledge base. 1161 | Does *not* delete the contents in the collections, only the collections themselves. 1162 | Accepts optional limit of how many collections to delete, defaults to 100. 1163 | Returns the collection identifiers and collection state, i.e. Deleted.`, 1164 | { 1165 | limit: z 1166 | .number() 1167 | .optional() 1168 | .default(100) 1169 | .describe( 1170 | "Limit the number of collections to be deleted. Defaults to 100." 1171 | ), 1172 | }, 1173 | async ({ limit }) => { 1174 | const client = new Graphlit(); 1175 | 1176 | try { 1177 | const filter: CollectionFilter = { 1178 | limit: limit, 1179 | }; 1180 | 1181 | const response = await client.deleteAllCollections(filter, true); 1182 | 1183 | return { 1184 | content: [ 1185 | { 1186 | type: "text", 1187 | text: JSON.stringify(response.deleteAllCollections, null, 2), 1188 | }, 1189 | ], 1190 | }; 1191 | } catch (err: unknown) { 1192 | const error = err as Error; 1193 | return { 1194 | content: [ 1195 | { 1196 | type: "text", 1197 | text: `Error: ${error.message}`, 1198 | }, 1199 | ], 1200 | isError: true, 1201 | }; 1202 | } 1203 | } 1204 | ); 1205 | 1206 | server.tool( 1207 | "deleteConversations", 1208 | `Deletes conversations from Graphlit knowledge base. 1209 | Accepts optional limit of how many conversations to delete, defaults to 100. 1210 | Returns the conversation identifiers and conversation state, i.e. Deleted.`, 1211 | { 1212 | limit: z 1213 | .number() 1214 | .optional() 1215 | .default(100) 1216 | .describe( 1217 | "Limit the number of conversations to be deleted. Defaults to 100." 1218 | ), 1219 | }, 1220 | async ({ limit }) => { 1221 | const client = new Graphlit(); 1222 | 1223 | try { 1224 | const filter: ConversationFilter = { 1225 | limit: limit, 1226 | }; 1227 | 1228 | const response = await client.deleteAllConversations(filter, true); 1229 | 1230 | return { 1231 | content: [ 1232 | { 1233 | type: "text", 1234 | text: JSON.stringify(response.deleteAllConversations, null, 2), 1235 | }, 1236 | ], 1237 | }; 1238 | } catch (err: unknown) { 1239 | const error = err as Error; 1240 | return { 1241 | content: [ 1242 | { 1243 | type: "text", 1244 | text: `Error: ${error.message}`, 1245 | }, 1246 | ], 1247 | isError: true, 1248 | }; 1249 | } 1250 | } 1251 | ); 1252 | 1253 | server.tool( 1254 | "deleteContents", 1255 | `Deletes contents from Graphlit knowledge base. 1256 | Accepts optional content type and file type filters to limit the contents which will be deleted. 1257 | Also accepts optional limit of how many contents to delete, defaults to 1000. 1258 | Returns the content identifiers and content state, i.e. Deleted.`, 1259 | { 1260 | contentType: z 1261 | .nativeEnum(ContentTypes) 1262 | .optional() 1263 | .describe( 1264 | "Content type filter, optional. One of: Email, Event, File, Issue, Message, Page, Post, Text." 1265 | ), 1266 | fileType: z 1267 | .nativeEnum(FileTypes) 1268 | .optional() 1269 | .describe( 1270 | "File type filter, optional. One of: Animation, Audio, Code, Data, Document, Drawing, Email, Geometry, Image, Package, PointCloud, Shape, Video." 1271 | ), 1272 | limit: z 1273 | .number() 1274 | .optional() 1275 | .default(1000) 1276 | .describe( 1277 | "Limit the number of contents to be deleted. Defaults to 1000." 1278 | ), 1279 | }, 1280 | async ({ contentType, fileType, limit }) => { 1281 | const client = new Graphlit(); 1282 | 1283 | try { 1284 | const filter: ContentFilter = { 1285 | types: contentType ? [contentType] : null, 1286 | fileTypes: fileType ? [fileType] : null, 1287 | limit: limit, 1288 | }; 1289 | 1290 | const response = await client.deleteAllContents(filter, true); 1291 | 1292 | return { 1293 | content: [ 1294 | { 1295 | type: "text", 1296 | text: JSON.stringify(response.deleteAllContents, null, 2), 1297 | }, 1298 | ], 1299 | }; 1300 | } catch (err: unknown) { 1301 | const error = err as Error; 1302 | return { 1303 | content: [ 1304 | { 1305 | type: "text", 1306 | text: `Error: ${error.message}`, 1307 | }, 1308 | ], 1309 | isError: true, 1310 | }; 1311 | } 1312 | } 1313 | ); 1314 | 1315 | server.tool( 1316 | "queryContents", 1317 | `Query contents from Graphlit knowledge base. Do *not* use for retrieving content by content identifier - retrieve content resource instead, with URI 'contents://{id}'. 1318 | Accepts optional content name, content type and file type for metadata filtering. 1319 | Accepts optional hybrid vector search query. 1320 | Accepts optional recency filter (defaults to null, meaning all time), and optional feed and collection identifiers to filter images by. 1321 | Accepts optional geo-location filter for search by latitude, longitude and optional distance radius. Images and videos taken with GPS enabled are searchable by geo-location. 1322 | Returns the matching contents, including their content resource URI to retrieve the complete Markdown text.`, 1323 | { 1324 | name: z.string().optional().describe("Textual match on content name."), 1325 | query: z.string().optional().describe("Search query."), 1326 | type: z 1327 | .nativeEnum(ContentTypes) 1328 | .optional() 1329 | .describe("Filter by content type."), 1330 | fileType: z 1331 | .nativeEnum(FileTypes) 1332 | .optional() 1333 | .describe("Filter by file type."), 1334 | inLast: z 1335 | .string() 1336 | .optional() 1337 | .describe( 1338 | "Recency filter for content ingested 'in last' timespan, optional. Should be ISO 8601 format, for example, 'PT1H' for last hour, 'P1D' for last day, 'P7D' for last week, 'P30D' for last month. Doesn't support weeks or months explicitly." 1339 | ), 1340 | feeds: z 1341 | .array(z.string()) 1342 | .optional() 1343 | .describe("Feed identifiers to filter contents by, optional."), 1344 | collections: z 1345 | .array(z.string()) 1346 | .optional() 1347 | .describe("Collection identifiers to filter contents by, optional."), 1348 | location: PointFilter.optional().describe( 1349 | "Geo-location filter for search by latitude, longitude and optional distance radius." 1350 | ), 1351 | limit: z 1352 | .number() 1353 | .optional() 1354 | .default(100) 1355 | .describe( 1356 | "Limit the number of contents to be returned. Defaults to 100." 1357 | ), 1358 | }, 1359 | async ({ 1360 | name, 1361 | query, 1362 | type, 1363 | fileType, 1364 | inLast, 1365 | feeds, 1366 | collections, 1367 | location, 1368 | limit, 1369 | }) => { 1370 | const client = new Graphlit(); 1371 | 1372 | try { 1373 | const filter: ContentFilter = { 1374 | name: name, 1375 | search: query, 1376 | searchType: SearchTypes.Hybrid, 1377 | types: type !== undefined ? [type] : undefined, 1378 | fileTypes: fileType !== undefined ? [fileType] : undefined, 1379 | feeds: feeds?.map((feed) => ({ id: feed })), 1380 | collections: collections?.map((collection) => ({ id: collection })), 1381 | location: location, 1382 | createdInLast: inLast, 1383 | limit: limit, 1384 | }; 1385 | const response = await client.queryContents(filter); 1386 | 1387 | const contents = response.contents?.results || []; 1388 | 1389 | return { 1390 | content: contents 1391 | .filter((content) => content !== null) 1392 | .map((content) => ({ 1393 | type: "text", 1394 | mimeType: "application/json", 1395 | text: JSON.stringify( 1396 | { 1397 | id: content.id, 1398 | relevance: content.relevance, 1399 | fileName: content.fileName, 1400 | resourceUri: `contents://${content.id}`, 1401 | uri: content.imageUri, 1402 | mimeType: content.mimeType, 1403 | }, 1404 | null, 1405 | 2 1406 | ), 1407 | })), 1408 | }; 1409 | } catch (err: unknown) { 1410 | const error = err as Error; 1411 | return { 1412 | content: [ 1413 | { 1414 | type: "text", 1415 | text: `Error: ${error.message}`, 1416 | }, 1417 | ], 1418 | isError: true, 1419 | }; 1420 | } 1421 | } 1422 | ); 1423 | 1424 | server.tool( 1425 | "queryCollections", 1426 | `Query collections from Graphlit knowledge base. Do *not* use for retrieving collection by collection identifier - retrieve collection resource instead, with URI 'collections://{id}'. 1427 | Accepts optional collection name for metadata filtering. 1428 | Returns the matching collections, including their collection resource URI to retrieve the collection contents.`, 1429 | { 1430 | name: z.string().optional().describe("Textual match on collection name."), 1431 | limit: z 1432 | .number() 1433 | .optional() 1434 | .default(100) 1435 | .describe( 1436 | "Limit the number of collections to be returned. Defaults to 100." 1437 | ), 1438 | }, 1439 | async ({ name, limit }) => { 1440 | const client = new Graphlit(); 1441 | 1442 | try { 1443 | const filter: CollectionFilter = { 1444 | name: name, 1445 | limit: limit, 1446 | }; 1447 | const response = await client.queryCollections(filter); 1448 | 1449 | const collections = response.collections?.results || []; 1450 | 1451 | return { 1452 | content: collections 1453 | .filter((collection) => collection !== null) 1454 | .map((collection) => ({ 1455 | type: "text", 1456 | mimeType: "application/json", 1457 | text: JSON.stringify( 1458 | { 1459 | id: collection.id, 1460 | relevance: collection.relevance, 1461 | resourceUri: `collections://${collection.id}`, 1462 | }, 1463 | null, 1464 | 2 1465 | ), 1466 | })), 1467 | }; 1468 | } catch (err: unknown) { 1469 | const error = err as Error; 1470 | return { 1471 | content: [ 1472 | { 1473 | type: "text", 1474 | text: `Error: ${error.message}`, 1475 | }, 1476 | ], 1477 | isError: true, 1478 | }; 1479 | } 1480 | } 1481 | ); 1482 | 1483 | server.tool( 1484 | "queryFeeds", 1485 | `Query feeds from Graphlit knowledge base. Do *not* use for retrieving feed by feed identifier - retrieve feed resource instead, with URI 'feeds://{id}'. 1486 | Accepts optional feed name and feed type for metadata filtering. 1487 | Returns the matching feeds, including their feed resource URI to retrieve the feed contents.`, 1488 | { 1489 | name: z.string().optional().describe("Textual match on feed name."), 1490 | type: z.nativeEnum(FeedTypes).optional().describe("Filter by feed type."), 1491 | limit: z 1492 | .number() 1493 | .optional() 1494 | .default(100) 1495 | .describe("Limit the number of feeds to be returned. Defaults to 100."), 1496 | }, 1497 | async ({ name, type, limit }) => { 1498 | const client = new Graphlit(); 1499 | 1500 | try { 1501 | const filter: FeedFilter = { 1502 | name: name, 1503 | types: type !== undefined ? [type] : undefined, 1504 | limit: limit, 1505 | }; 1506 | const response = await client.queryFeeds(filter); 1507 | 1508 | const feeds = response.feeds?.results || []; 1509 | 1510 | return { 1511 | content: feeds 1512 | .filter((feed) => feed !== null) 1513 | .map((feed) => ({ 1514 | type: "text", 1515 | mimeType: "application/json", 1516 | text: JSON.stringify( 1517 | { 1518 | id: feed.id, 1519 | relevance: feed.relevance, 1520 | resourceUri: `feeds://${feed.id}`, 1521 | }, 1522 | null, 1523 | 2 1524 | ), 1525 | })), 1526 | }; 1527 | } catch (err: unknown) { 1528 | const error = err as Error; 1529 | return { 1530 | content: [ 1531 | { 1532 | type: "text", 1533 | text: `Error: ${error.message}`, 1534 | }, 1535 | ], 1536 | isError: true, 1537 | }; 1538 | } 1539 | } 1540 | ); 1541 | 1542 | server.tool( 1543 | "queryConversations", 1544 | `Query conversations from Graphlit knowledge base. Do *not* use for retrieving conversation by conversation identifier - retrieve conversation resource instead, with URI 'conversations://{id}'. 1545 | Accepts optional hybrid vector search query. 1546 | Accepts optional recency filter (defaults to null, meaning all time). 1547 | Returns the matching conversations, including their conversation resource URI to retrieve the complete conversation message history.`, 1548 | { 1549 | query: z.string().optional().describe("Search query."), 1550 | inLast: z 1551 | .string() 1552 | .optional() 1553 | .describe( 1554 | "Recency filter for conversations created 'in last' timespan, optional. Should be ISO 8601 format, for example, 'PT1H' for last hour, 'P1D' for last day, 'P7D' for last week, 'P30D' for last month. Doesn't support weeks or months explicitly." 1555 | ), 1556 | limit: z 1557 | .number() 1558 | .optional() 1559 | .default(100) 1560 | .describe( 1561 | "Limit the number of conversations to be returned. Defaults to 100." 1562 | ), 1563 | }, 1564 | async ({ query, inLast, limit }) => { 1565 | const client = new Graphlit(); 1566 | 1567 | try { 1568 | const filter: ConversationFilter = { 1569 | search: query, 1570 | searchType: SearchTypes.Hybrid, 1571 | createdInLast: inLast, 1572 | limit: limit, 1573 | }; 1574 | const response = await client.queryConversations(filter); 1575 | 1576 | const conversations = response.conversations?.results || []; 1577 | 1578 | return { 1579 | content: conversations 1580 | .filter((conversation) => conversation !== null) 1581 | .map((conversation) => ({ 1582 | type: "text", 1583 | mimeType: "application/json", 1584 | text: JSON.stringify( 1585 | { 1586 | id: conversation.id, 1587 | relevance: conversation.relevance, 1588 | resourceUri: `conversations://${conversation.id}`, 1589 | }, 1590 | null, 1591 | 2 1592 | ), 1593 | })), 1594 | }; 1595 | } catch (err: unknown) { 1596 | const error = err as Error; 1597 | return { 1598 | content: [ 1599 | { 1600 | type: "text", 1601 | text: `Error: ${error.message}`, 1602 | }, 1603 | ], 1604 | isError: true, 1605 | }; 1606 | } 1607 | } 1608 | ); 1609 | 1610 | server.tool( 1611 | "isContentDone", 1612 | `Check if content has completed asynchronous ingestion. 1613 | Accepts a content identifier which was returned from one of the non-feed ingestion tools, like ingestUrl. 1614 | Returns whether the content is done or not.`, 1615 | { 1616 | id: z.string().describe("Content identifier."), 1617 | }, 1618 | async ({ id }) => { 1619 | const client = new Graphlit(); 1620 | 1621 | try { 1622 | const response = await client.isContentDone(id); 1623 | 1624 | return { 1625 | content: [ 1626 | { 1627 | type: "text", 1628 | text: JSON.stringify( 1629 | { done: response.isContentDone?.result }, 1630 | null, 1631 | 2 1632 | ), 1633 | }, 1634 | ], 1635 | }; 1636 | } catch (err: unknown) { 1637 | const error = err as Error; 1638 | return { 1639 | content: [ 1640 | { 1641 | type: "text", 1642 | text: `Error: ${error.message}`, 1643 | }, 1644 | ], 1645 | isError: true, 1646 | }; 1647 | } 1648 | } 1649 | ); 1650 | 1651 | server.tool( 1652 | "isFeedDone", 1653 | `Check if an asynchronous feed has completed ingesting all the available content. 1654 | Accepts a feed identifier which was returned from one of the ingestion tools, like ingestGoogleDriveFiles. 1655 | Returns whether the feed is done or not.`, 1656 | { 1657 | id: z.string().describe("Feed identifier."), 1658 | }, 1659 | async ({ id }) => { 1660 | const client = new Graphlit(); 1661 | 1662 | try { 1663 | const response = await client.isFeedDone(id); 1664 | 1665 | return { 1666 | content: [ 1667 | { 1668 | type: "text", 1669 | text: JSON.stringify( 1670 | { done: response.isFeedDone?.result }, 1671 | null, 1672 | 2 1673 | ), 1674 | }, 1675 | ], 1676 | }; 1677 | } catch (err: unknown) { 1678 | const error = err as Error; 1679 | return { 1680 | content: [ 1681 | { 1682 | type: "text", 1683 | text: `Error: ${error.message}`, 1684 | }, 1685 | ], 1686 | isError: true, 1687 | }; 1688 | } 1689 | } 1690 | ); 1691 | 1692 | /* 1693 | server.tool( 1694 | "listMicrosoftTeamsTeams", 1695 | `Lists available Microsoft Teams teams. 1696 | Requires environment variables to be configured: MICROSOFT_TEAMS_CLIENT_ID, MICROSOFT_TEAMS_CLIENT_SECRET, MICROSOFT_TEAMS_REFRESH_TOKEN. 1697 | Returns a list of Microsoft Teams teams, where the team identifier can be used with listMicrosoftTeamsChannels to enumerate Microsoft Teams channels.`, 1698 | { 1699 | }, 1700 | async ({ }) => { 1701 | const client = new Graphlit(); 1702 | 1703 | try { 1704 | const clientId = process.env.MICROSOFT_TEAMS_CLIENT_ID; 1705 | if (!clientId) { 1706 | console.error("Please set MICROSOFT_TEAMS_CLIENT_ID environment variable."); 1707 | process.exit(1); 1708 | } 1709 | 1710 | const clientSecret = process.env.MICROSOFT_TEAMS_CLIENT_SECRET; 1711 | if (!clientSecret) { 1712 | console.error("Please set MICROSOFT_TEAMS_CLIENT_SECRET environment variable."); 1713 | process.exit(1); 1714 | } 1715 | 1716 | const refreshToken = process.env.MICROSOFT_TEAMS_REFRESH_TOKEN; 1717 | if (!refreshToken) { 1718 | console.error("Please set MICROSOFT_TEAMS_REFRESH_TOKEN environment variable."); 1719 | process.exit(1); 1720 | } 1721 | 1722 | // REVIEW: client ID/secret not exposed in SDK 1723 | const response = await client.queryMicrosoftTeamsTeams({ 1724 | //clientId: clientId, 1725 | //clientSecret: clientSecret, 1726 | refreshToken: refreshToken, 1727 | }); 1728 | 1729 | return { 1730 | content: [{ 1731 | type: "text", 1732 | text: JSON.stringify(response.microsoftTeamsTeams?.results, null, 2) 1733 | }] 1734 | }; 1735 | 1736 | } catch (err: unknown) { 1737 | const error = err as Error; 1738 | return { 1739 | content: [{ 1740 | type: "text", 1741 | text: `Error: ${error.message}` 1742 | }], 1743 | isError: true 1744 | }; 1745 | } 1746 | } 1747 | ); 1748 | 1749 | server.tool( 1750 | "listMicrosoftTeamsChannels", 1751 | `Lists available Microsoft Teams channels. 1752 | Requires environment variables to be configured: MICROSOFT_TEAMS_CLIENT_ID, MICROSOFT_TEAMS_CLIENT_SECRET, MICROSOFT_TEAMS_REFRESH_TOKEN. 1753 | Returns a list of Microsoft Teams channels, where the channel identifier can be used with ingestMicrosoftTeamsMessages to ingest messages into Graphlit knowledge base.`, 1754 | { 1755 | teamId: z.string().describe("Microsoft Teams team identifier.") 1756 | }, 1757 | async ({ teamId }) => { 1758 | const client = new Graphlit(); 1759 | 1760 | try { 1761 | const clientId = process.env.MICROSOFT_TEAMS_CLIENT_ID; 1762 | if (!clientId) { 1763 | console.error("Please set MICROSOFT_TEAMS_CLIENT_ID environment variable."); 1764 | process.exit(1); 1765 | } 1766 | 1767 | const clientSecret = process.env.MICROSOFT_TEAMS_CLIENT_SECRET; 1768 | if (!clientSecret) { 1769 | console.error("Please set MICROSOFT_TEAMS_CLIENT_SECRET environment variable."); 1770 | process.exit(1); 1771 | } 1772 | 1773 | const refreshToken = process.env.MICROSOFT_TEAMS_REFRESH_TOKEN; 1774 | if (!refreshToken) { 1775 | console.error("Please set MICROSOFT_TEAMS_REFRESH_TOKEN environment variable."); 1776 | process.exit(1); 1777 | } 1778 | 1779 | // REVIEW: client ID/secret not exposed in SDK 1780 | const response = await client.queryMicrosoftTeamsChannels({ 1781 | //clientId: clientId, 1782 | //clientSecret: clientSecret, 1783 | refreshToken: refreshToken, 1784 | }, teamId); 1785 | 1786 | return { 1787 | content: [{ 1788 | type: "text", 1789 | text: JSON.stringify(response.microsoftTeamsChannels?.results, null, 2) 1790 | }] 1791 | }; 1792 | 1793 | } catch (err: unknown) { 1794 | const error = err as Error; 1795 | return { 1796 | content: [{ 1797 | type: "text", 1798 | text: `Error: ${error.message}` 1799 | }], 1800 | isError: true 1801 | }; 1802 | } 1803 | } 1804 | ); 1805 | */ 1806 | 1807 | server.tool( 1808 | "listNotionDatabases", 1809 | `Lists available Notion databases. 1810 | Requires environment variable to be configured: NOTION_API_KEY. 1811 | Returns a list of Notion databases, where the database identifier can be used with ingestNotionPages to ingest pages into Graphlit knowledge base.`, 1812 | {}, 1813 | async ({}) => { 1814 | const client = new Graphlit(); 1815 | 1816 | try { 1817 | const token = process.env.NOTION_API_KEY; 1818 | if (!token) { 1819 | console.error("Please set NOTION_API_KEY environment variable."); 1820 | process.exit(1); 1821 | } 1822 | 1823 | const response = await client.queryNotionDatabases({ 1824 | token: token, 1825 | }); 1826 | 1827 | return { 1828 | content: [ 1829 | { 1830 | type: "text", 1831 | text: JSON.stringify(response.notionDatabases?.results, null, 2), 1832 | }, 1833 | ], 1834 | }; 1835 | } catch (err: unknown) { 1836 | const error = err as Error; 1837 | return { 1838 | content: [ 1839 | { 1840 | type: "text", 1841 | text: `Error: ${error.message}`, 1842 | }, 1843 | ], 1844 | isError: true, 1845 | }; 1846 | } 1847 | } 1848 | ); 1849 | 1850 | server.tool( 1851 | "listLinearProjects", 1852 | `Lists available Linear projects. 1853 | Requires environment variable to be configured: LINEAR_API_KEY. 1854 | Returns a list of Linear projects, where the project name can be used with ingestLinearIssues to ingest issues into Graphlit knowledge base.`, 1855 | {}, 1856 | async ({}) => { 1857 | const client = new Graphlit(); 1858 | 1859 | try { 1860 | const apiKey = process.env.LINEAR_API_KEY; 1861 | if (!apiKey) { 1862 | console.error("Please set LINEAR_API_KEY environment variable."); 1863 | process.exit(1); 1864 | } 1865 | 1866 | const response = await client.queryLinearProjects({ 1867 | key: apiKey, 1868 | }); 1869 | 1870 | return { 1871 | content: [ 1872 | { 1873 | type: "text", 1874 | text: JSON.stringify(response.linearProjects?.results, null, 2), 1875 | }, 1876 | ], 1877 | }; 1878 | } catch (err: unknown) { 1879 | const error = err as Error; 1880 | return { 1881 | content: [ 1882 | { 1883 | type: "text", 1884 | text: `Error: ${error.message}`, 1885 | }, 1886 | ], 1887 | isError: true, 1888 | }; 1889 | } 1890 | } 1891 | ); 1892 | 1893 | server.tool( 1894 | "listSlackChannels", 1895 | `Lists available Slack channels. 1896 | Requires environment variable to be configured: SLACK_BOT_TOKEN. 1897 | Returns a list of Slack channels, where the channel name can be used with ingestSlackMessages to ingest messages into Graphlit knowledge base.`, 1898 | {}, 1899 | async ({}) => { 1900 | const client = new Graphlit(); 1901 | 1902 | try { 1903 | const botToken = process.env.SLACK_BOT_TOKEN; 1904 | if (!botToken) { 1905 | console.error("Please set SLACK_BOT_TOKEN environment variable."); 1906 | process.exit(1); 1907 | } 1908 | 1909 | const response = await client.querySlackChannels({ 1910 | token: botToken, 1911 | }); 1912 | 1913 | return { 1914 | content: [ 1915 | { 1916 | type: "text", 1917 | text: JSON.stringify(response.slackChannels?.results, null, 2), 1918 | }, 1919 | ], 1920 | }; 1921 | } catch (err: unknown) { 1922 | const error = err as Error; 1923 | return { 1924 | content: [ 1925 | { 1926 | type: "text", 1927 | text: `Error: ${error.message}`, 1928 | }, 1929 | ], 1930 | isError: true, 1931 | }; 1932 | } 1933 | } 1934 | ); 1935 | 1936 | server.tool( 1937 | "listSharePointLibraries", 1938 | `Lists available SharePoint libraries. 1939 | Requires environment variables to be configured: SHAREPOINT_CLIENT_ID, SHAREPOINT_CLIENT_SECRET, SHAREPOINT_REFRESH_TOKEN. 1940 | Returns a list of SharePoint libraries, where the selected libraryId can be used with listSharePointFolders to enumerate SharePoint folders in a library.`, 1941 | {}, 1942 | async ({}) => { 1943 | const client = new Graphlit(); 1944 | 1945 | try { 1946 | const clientId = process.env.SHAREPOINT_CLIENT_ID; 1947 | if (!clientId) { 1948 | console.error( 1949 | "Please set SHAREPOINT_CLIENT_ID environment variable." 1950 | ); 1951 | process.exit(1); 1952 | } 1953 | 1954 | const clientSecret = process.env.SHAREPOINT_CLIENT_SECRET; 1955 | if (!clientSecret) { 1956 | console.error( 1957 | "Please set SHAREPOINT_CLIENT_SECRET environment variable." 1958 | ); 1959 | process.exit(1); 1960 | } 1961 | 1962 | const refreshToken = process.env.SHAREPOINT_REFRESH_TOKEN; 1963 | if (!refreshToken) { 1964 | console.error( 1965 | "Please set SHAREPOINT_REFRESH_TOKEN environment variable." 1966 | ); 1967 | process.exit(1); 1968 | } 1969 | 1970 | const response = await client.querySharePointLibraries({ 1971 | authenticationType: SharePointAuthenticationTypes.User, 1972 | clientId: clientId, 1973 | clientSecret: clientSecret, 1974 | refreshToken: refreshToken, 1975 | }); 1976 | 1977 | return { 1978 | content: [ 1979 | { 1980 | type: "text", 1981 | text: JSON.stringify( 1982 | response.sharePointLibraries?.results, 1983 | null, 1984 | 2 1985 | ), 1986 | }, 1987 | ], 1988 | }; 1989 | } catch (err: unknown) { 1990 | const error = err as Error; 1991 | return { 1992 | content: [ 1993 | { 1994 | type: "text", 1995 | text: `Error: ${error.message}`, 1996 | }, 1997 | ], 1998 | isError: true, 1999 | }; 2000 | } 2001 | } 2002 | ); 2003 | 2004 | server.tool( 2005 | "listSharePointFolders", 2006 | `Lists available SharePoint folders. 2007 | Requires environment variables to be configured: SHAREPOINT_CLIENT_ID, SHAREPOINT_CLIENT_SECRET, SHAREPOINT_REFRESH_TOKEN. 2008 | Returns a list of SharePoint folders, which can be used with ingestSharePointFiles to ingest files into Graphlit knowledge base.`, 2009 | { 2010 | libraryId: z.string().describe("SharePoint library identifier."), 2011 | }, 2012 | async ({ libraryId }) => { 2013 | const client = new Graphlit(); 2014 | 2015 | try { 2016 | const clientId = process.env.SHAREPOINT_CLIENT_ID; 2017 | if (!clientId) { 2018 | console.error( 2019 | "Please set SHAREPOINT_CLIENT_ID environment variable." 2020 | ); 2021 | process.exit(1); 2022 | } 2023 | 2024 | const clientSecret = process.env.SHAREPOINT_CLIENT_SECRET; 2025 | if (!clientSecret) { 2026 | console.error( 2027 | "Please set SHAREPOINT_CLIENT_SECRET environment variable." 2028 | ); 2029 | process.exit(1); 2030 | } 2031 | 2032 | const refreshToken = process.env.SHAREPOINT_REFRESH_TOKEN; 2033 | if (!refreshToken) { 2034 | console.error( 2035 | "Please set SHAREPOINT_REFRESH_TOKEN environment variable." 2036 | ); 2037 | process.exit(1); 2038 | } 2039 | 2040 | const response = await client.querySharePointFolders( 2041 | { 2042 | authenticationType: SharePointAuthenticationTypes.User, 2043 | clientId: clientId, 2044 | clientSecret: clientSecret, 2045 | refreshToken: refreshToken, 2046 | }, 2047 | libraryId 2048 | ); 2049 | 2050 | return { 2051 | content: [ 2052 | { 2053 | type: "text", 2054 | text: JSON.stringify( 2055 | response.sharePointFolders?.results, 2056 | null, 2057 | 2 2058 | ), 2059 | }, 2060 | ], 2061 | }; 2062 | } catch (err: unknown) { 2063 | const error = err as Error; 2064 | return { 2065 | content: [ 2066 | { 2067 | type: "text", 2068 | text: `Error: ${error.message}`, 2069 | }, 2070 | ], 2071 | isError: true, 2072 | }; 2073 | } 2074 | } 2075 | ); 2076 | 2077 | server.tool( 2078 | "ingestSharePointFiles", 2079 | `Ingests files from SharePoint library into Graphlit knowledge base. 2080 | Accepts a SharePoint libraryId and an optional folderId to ingest files from a specific SharePoint folder. 2081 | Libraries can be enumerated with listSharePointLibraries and library folders with listSharePointFolders. 2082 | Requires environment variables to be configured: SHAREPOINT_ACCOUNT_NAME, SHAREPOINT_CLIENT_ID, SHAREPOINT_CLIENT_SECRET, SHAREPOINT_REFRESH_TOKEN. 2083 | Accepts an optional read limit for the number of files to ingest. 2084 | Executes asynchronously, creates SharePoint feed, and returns the feed identifier. Optionally creates a recurring feed that checks for new content every 15 minutes when 'recurring' is set to true.`, 2085 | { 2086 | libraryId: z.string().describe("SharePoint library identifier."), 2087 | folderId: z 2088 | .string() 2089 | .optional() 2090 | .describe("SharePoint folder identifier, optional."), 2091 | readLimit: z 2092 | .number() 2093 | .optional() 2094 | .describe("Number of files to ingest, optional. Defaults to 100."), 2095 | recurring: z 2096 | .boolean() 2097 | .optional() 2098 | .default(false) 2099 | .describe( 2100 | "Whether to create a recurring feed that checks for new content. Defaults to false (one-time execution). When true, isFeedDone is not needed for completion polling." 2101 | ), 2102 | repeatInterval: z 2103 | .string() 2104 | .optional() 2105 | .default("PT15M") 2106 | .describe( 2107 | "ISO 8601 duration for recurring interval (e.g., 'PT5M' for 5 minutes, 'PT15M' for 15 minutes, 'PT1H' for 1 hour). Must be at least PT5M. Only used when recurring is true." 2108 | ), 2109 | }, 2110 | async ({ libraryId, folderId, readLimit, recurring, repeatInterval }) => { 2111 | const client = new Graphlit(); 2112 | 2113 | try { 2114 | const accountName = process.env.SHAREPOINT_ACCOUNT_NAME; 2115 | if (!accountName) { 2116 | console.error( 2117 | "Please set SHAREPOINT_ACCOUNT_NAME environment variable." 2118 | ); 2119 | process.exit(1); 2120 | } 2121 | 2122 | const clientId = process.env.SHAREPOINT_CLIENT_ID; 2123 | if (!clientId) { 2124 | console.error( 2125 | "Please set SHAREPOINT_CLIENT_ID environment variable." 2126 | ); 2127 | process.exit(1); 2128 | } 2129 | 2130 | const clientSecret = process.env.SHAREPOINT_CLIENT_SECRET; 2131 | if (!clientSecret) { 2132 | console.error( 2133 | "Please set SHAREPOINT_CLIENT_SECRET environment variable." 2134 | ); 2135 | process.exit(1); 2136 | } 2137 | 2138 | const refreshToken = process.env.SHAREPOINT_REFRESH_TOKEN; 2139 | if (!refreshToken) { 2140 | console.error( 2141 | "Please set SHAREPOINT_REFRESH_TOKEN environment variable." 2142 | ); 2143 | process.exit(1); 2144 | } 2145 | 2146 | const response = await client.createFeed({ 2147 | name: `SharePoint`, 2148 | type: FeedTypes.Site, 2149 | site: { 2150 | type: FeedServiceTypes.SharePoint, 2151 | sharePoint: { 2152 | authenticationType: SharePointAuthenticationTypes.User, 2153 | accountName: accountName, 2154 | clientId: clientId, 2155 | clientSecret: clientSecret, 2156 | refreshToken: refreshToken, 2157 | libraryId: libraryId, 2158 | folderId: folderId, 2159 | }, 2160 | isRecursive: true, 2161 | readLimit: readLimit || 100, 2162 | }, 2163 | schedulePolicy: recurring 2164 | ? { 2165 | recurrenceType: TimedPolicyRecurrenceTypes.Repeat, 2166 | repeatInterval: repeatInterval || "PT15M", 2167 | } 2168 | : undefined, 2169 | }); 2170 | 2171 | return { 2172 | content: [ 2173 | { 2174 | type: "text", 2175 | text: JSON.stringify({ id: response.createFeed?.id }, null, 2), 2176 | }, 2177 | ], 2178 | }; 2179 | } catch (err: unknown) { 2180 | const error = err as Error; 2181 | return { 2182 | content: [ 2183 | { 2184 | type: "text", 2185 | text: `Error: ${error.message}`, 2186 | }, 2187 | ], 2188 | isError: true, 2189 | }; 2190 | } 2191 | } 2192 | ); 2193 | 2194 | server.tool( 2195 | "ingestOneDriveFiles", 2196 | `Ingests files from OneDrive into Graphlit knowledge base. 2197 | Accepts optional OneDrive folder identifier, and an optional read limit for the number of files to ingest. 2198 | If no folder identifier provided, ingests files from root OneDrive folder. 2199 | Requires environment variables to be configured: ONEDRIVE_CLIENT_ID, ONEDRIVE_CLIENT_SECRET, ONEDRIVE_REFRESH_TOKEN. 2200 | Executes asynchronously, creates OneDrive feed, and returns the feed identifier. Optionally creates a recurring feed that checks for new content every 15 minutes when 'recurring' is set to true.`, 2201 | { 2202 | folderId: z 2203 | .string() 2204 | .optional() 2205 | .describe("OneDrive folder identifier, optional."), 2206 | readLimit: z 2207 | .number() 2208 | .optional() 2209 | .describe("Number of files to ingest, optional. Defaults to 100."), 2210 | recurring: z 2211 | .boolean() 2212 | .optional() 2213 | .default(false) 2214 | .describe( 2215 | "Whether to create a recurring feed that checks for new content. Defaults to false (one-time execution). When true, isFeedDone is not needed." 2216 | ), 2217 | repeatInterval: z 2218 | .string() 2219 | .optional() 2220 | .default("PT15M") 2221 | .describe( 2222 | "ISO 8601 duration for recurring interval (e.g., 'PT5M' for 5 minutes, 'PT15M' for 15 minutes, 'PT1H' for 1 hour). Must be at least PT5M. Only used when recurring is true." 2223 | ), 2224 | }, 2225 | async ({ folderId, readLimit, recurring, repeatInterval }) => { 2226 | const client = new Graphlit(); 2227 | 2228 | try { 2229 | const clientId = process.env.ONEDRIVE_CLIENT_ID; 2230 | if (!clientId) { 2231 | console.error("Please set ONEDRIVE_CLIENT_ID environment variable."); 2232 | process.exit(1); 2233 | } 2234 | 2235 | const clientSecret = process.env.ONEDRIVE_CLIENT_SECRET; 2236 | if (!clientSecret) { 2237 | console.error( 2238 | "Please set ONEDRIVE_CLIENT_SECRET environment variable." 2239 | ); 2240 | process.exit(1); 2241 | } 2242 | 2243 | const refreshToken = process.env.ONEDRIVE_REFRESH_TOKEN; 2244 | if (!refreshToken) { 2245 | console.error( 2246 | "Please set ONEDRIVE_REFRESH_TOKEN environment variable." 2247 | ); 2248 | process.exit(1); 2249 | } 2250 | 2251 | const response = await client.createFeed({ 2252 | name: `OneDrive`, 2253 | type: FeedTypes.Site, 2254 | site: { 2255 | type: FeedServiceTypes.OneDrive, 2256 | oneDrive: { 2257 | folderId: folderId, 2258 | clientId: clientId, 2259 | clientSecret: clientSecret, 2260 | refreshToken: refreshToken, 2261 | }, 2262 | isRecursive: true, 2263 | readLimit: readLimit || 100, 2264 | }, 2265 | schedulePolicy: recurring 2266 | ? { 2267 | recurrenceType: TimedPolicyRecurrenceTypes.Repeat, 2268 | repeatInterval: repeatInterval || "PT15M", 2269 | } 2270 | : undefined, 2271 | }); 2272 | 2273 | return { 2274 | content: [ 2275 | { 2276 | type: "text", 2277 | text: JSON.stringify({ id: response.createFeed?.id }, null, 2), 2278 | }, 2279 | ], 2280 | }; 2281 | } catch (err: unknown) { 2282 | const error = err as Error; 2283 | return { 2284 | content: [ 2285 | { 2286 | type: "text", 2287 | text: `Error: ${error.message}`, 2288 | }, 2289 | ], 2290 | isError: true, 2291 | }; 2292 | } 2293 | } 2294 | ); 2295 | 2296 | server.tool( 2297 | "ingestGoogleDriveFiles", 2298 | `Ingests files from Google Drive into Graphlit knowledge base. 2299 | Accepts optional Google Drive folder identifier, and an optional read limit for the number of files to ingest. 2300 | For example, with Google Drive URI (https://drive.google.com/drive/u/0/folders/32tzhRD12KDh2hXABY8OZRFv7Smy8WBkQ), the folder identifier is 32tzhRD12KDh2hXABY8OZRFv7Smy8WBkQ. 2301 | If no folder identifier provided, ingests files from root Google Drive folder. 2302 | Requires environment variables to be configured: GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON -or- GOOGLE_DRIVE_CLIENT_ID, GOOGLE_DRIVE_CLIENT_SECRET, GOOGLE_DRIVE_REFRESH_TOKEN. 2303 | If service account JSON is provided, uses service account authentication. Else, uses user authentication. 2304 | Executes asynchronously, creates Google Drive feed, and returns the feed identifier. Optionally creates a recurring feed that checks for new content every 15 minutes when 'recurring' is set to true.`, 2305 | { 2306 | folderId: z 2307 | .string() 2308 | .optional() 2309 | .describe("Google Drive folder identifier, optional."), 2310 | readLimit: z 2311 | .number() 2312 | .optional() 2313 | .describe("Number of files to ingest, optional. Defaults to 100."), 2314 | recurring: z 2315 | .boolean() 2316 | .optional() 2317 | .default(false) 2318 | .describe( 2319 | "Whether to create a recurring feed that checks for new content. Defaults to false (one-time execution). When true, isFeedDone is not needed." 2320 | ), 2321 | repeatInterval: z 2322 | .string() 2323 | .optional() 2324 | .default("PT15M") 2325 | .describe( 2326 | "ISO 8601 duration for recurring interval (e.g., 'PT5M' for 5 minutes, 'PT15M' for 15 minutes, 'PT1H' for 1 hour). Must be at least PT5M. Only used when recurring is true." 2327 | ), 2328 | }, 2329 | async ({ folderId, readLimit, recurring, repeatInterval }) => { 2330 | const client = new Graphlit(); 2331 | 2332 | try { 2333 | var clientId; 2334 | var clientSecret; 2335 | var refreshToken; 2336 | var authenticationType = GoogleDriveAuthenticationTypes.ServiceAccount; 2337 | 2338 | const serviceAccountJson = 2339 | process.env.GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON; 2340 | 2341 | if (!serviceAccountJson) { 2342 | authenticationType = GoogleDriveAuthenticationTypes.User; 2343 | 2344 | clientId = process.env.GOOGLE_DRIVE_CLIENT_ID; 2345 | if (!clientId) { 2346 | console.error( 2347 | "Please set GOOGLE_DRIVE_CLIENT_ID environment variable." 2348 | ); 2349 | process.exit(1); 2350 | } 2351 | 2352 | clientSecret = process.env.GOOGLE_DRIVE_CLIENT_SECRET; 2353 | if (!clientSecret) { 2354 | console.error( 2355 | "Please set GOOGLE_DRIVE_CLIENT_SECRET environment variable." 2356 | ); 2357 | process.exit(1); 2358 | } 2359 | 2360 | refreshToken = process.env.GOOGLE_DRIVE_REFRESH_TOKEN; 2361 | if (!refreshToken) { 2362 | console.error( 2363 | "Please set GOOGLE_DRIVE_REFRESH_TOKEN environment variable." 2364 | ); 2365 | process.exit(1); 2366 | } 2367 | } 2368 | 2369 | const response = await client.createFeed({ 2370 | name: `Google Drive`, 2371 | type: FeedTypes.Site, 2372 | site: { 2373 | type: FeedServiceTypes.GoogleDrive, 2374 | googleDrive: { 2375 | authenticationType: authenticationType, 2376 | folderId: folderId, 2377 | clientId: clientId, 2378 | clientSecret: clientSecret, 2379 | refreshToken: refreshToken, 2380 | serviceAccountJson: serviceAccountJson, 2381 | }, 2382 | isRecursive: true, 2383 | readLimit: readLimit || 100, 2384 | }, 2385 | schedulePolicy: recurring 2386 | ? { 2387 | recurrenceType: TimedPolicyRecurrenceTypes.Repeat, 2388 | repeatInterval: repeatInterval || "PT15M", 2389 | } 2390 | : undefined, 2391 | }); 2392 | 2393 | return { 2394 | content: [ 2395 | { 2396 | type: "text", 2397 | text: JSON.stringify({ id: response.createFeed?.id }, null, 2), 2398 | }, 2399 | ], 2400 | }; 2401 | } catch (err: unknown) { 2402 | const error = err as Error; 2403 | return { 2404 | content: [ 2405 | { 2406 | type: "text", 2407 | text: `Error: ${error.message}`, 2408 | }, 2409 | ], 2410 | isError: true, 2411 | }; 2412 | } 2413 | } 2414 | ); 2415 | 2416 | server.tool( 2417 | "ingestDropboxFiles", 2418 | `Ingests files from Dropbox into Graphlit knowledge base. 2419 | Accepts optional relative path to Dropbox folder (i.e. /Pictures), and an optional read limit for the number of files to ingest. 2420 | If no path provided, ingests files from root Dropbox folder. 2421 | Requires environment variables to be configured: DROPBOX_APP_KEY, DROPBOX_APP_SECRET, DROPBOX_REDIRECT_URI, DROPBOX_REFRESH_TOKEN. 2422 | Executes asynchronously, creates Dropbox feed, and returns the feed identifier. Optionally creates a recurring feed that checks for new content every 15 minutes when 'recurring' is set to true.`, 2423 | { 2424 | path: z 2425 | .string() 2426 | .optional() 2427 | .describe("Relative path to Dropbox folder, optional."), 2428 | readLimit: z 2429 | .number() 2430 | .optional() 2431 | .describe("Number of files to ingest, optional. Defaults to 100."), 2432 | recurring: z 2433 | .boolean() 2434 | .optional() 2435 | .default(false) 2436 | .describe( 2437 | "Whether to create a recurring feed that checks for new content. Defaults to false (one-time execution). When true, isFeedDone is not needed." 2438 | ), 2439 | repeatInterval: z 2440 | .string() 2441 | .optional() 2442 | .default("PT15M") 2443 | .describe( 2444 | "ISO 8601 duration for recurring interval (e.g., 'PT5M' for 5 minutes, 'PT15M' for 15 minutes, 'PT1H' for 1 hour). Must be at least PT5M. Only used when recurring is true." 2445 | ), 2446 | }, 2447 | async ({ path, readLimit, recurring, repeatInterval }) => { 2448 | const client = new Graphlit(); 2449 | 2450 | try { 2451 | const appKey = process.env.DROPBOX_APP_KEY; 2452 | if (!appKey) { 2453 | console.error("Please set DROPBOX_APP_KEY environment variable."); 2454 | process.exit(1); 2455 | } 2456 | 2457 | const appSecret = process.env.DROPBOX_APP_SECRET; 2458 | if (!appSecret) { 2459 | console.error("Please set DROPBOX_APP_SECRET environment variable."); 2460 | process.exit(1); 2461 | } 2462 | 2463 | const redirectUri = process.env.DROPBOX_REDIRECT_URI; 2464 | if (!redirectUri) { 2465 | console.error( 2466 | "Please set DROPBOX_REDIRECT_URI environment variable." 2467 | ); 2468 | process.exit(1); 2469 | } 2470 | 2471 | const refreshToken = process.env.DROPBOX_REFRESH_TOKEN; 2472 | if (!refreshToken) { 2473 | console.error( 2474 | "Please set DROPBOX_REFRESH_TOKEN environment variable." 2475 | ); 2476 | process.exit(1); 2477 | } 2478 | 2479 | const response = await client.createFeed({ 2480 | name: `Dropbox`, 2481 | type: FeedTypes.Site, 2482 | site: { 2483 | type: FeedServiceTypes.Dropbox, 2484 | dropbox: { 2485 | path: path, 2486 | appKey: appKey, 2487 | appSecret: appSecret, 2488 | redirectUri: redirectUri, 2489 | refreshToken: refreshToken, 2490 | }, 2491 | isRecursive: true, 2492 | readLimit: readLimit || 100, 2493 | }, 2494 | schedulePolicy: recurring 2495 | ? { 2496 | recurrenceType: TimedPolicyRecurrenceTypes.Repeat, 2497 | repeatInterval: repeatInterval || "PT15M", 2498 | } 2499 | : undefined, 2500 | }); 2501 | 2502 | return { 2503 | content: [ 2504 | { 2505 | type: "text", 2506 | text: JSON.stringify({ id: response.createFeed?.id }, null, 2), 2507 | }, 2508 | ], 2509 | }; 2510 | } catch (err: unknown) { 2511 | const error = err as Error; 2512 | return { 2513 | content: [ 2514 | { 2515 | type: "text", 2516 | text: `Error: ${error.message}`, 2517 | }, 2518 | ], 2519 | isError: true, 2520 | }; 2521 | } 2522 | } 2523 | ); 2524 | 2525 | server.tool( 2526 | "ingestBoxFiles", 2527 | `Ingests files from Box into Graphlit knowledge base. 2528 | Accepts optional Box folder identifier, and an optional read limit for the number of files to ingest. 2529 | If no folder identifier provided, ingests files from root Box folder (i.e. "0"). 2530 | Folder identifier can be inferred from Box URL. https://app.box.com/folder/123456 -> folder identifier is "123456". 2531 | Requires environment variables to be configured: BOX_CLIENT_ID, BOX_CLIENT_SECRET, BOX_REDIRECT_URI, BOX_REFRESH_TOKEN. 2532 | Executes asynchronously, creates Box feed, and returns the feed identifier. Optionally creates a recurring feed that checks for new content every 15 minutes when 'recurring' is set to true.`, 2533 | { 2534 | folderId: z 2535 | .string() 2536 | .optional() 2537 | .default("0") 2538 | .describe("Box folder identifier, optional. Defaults to root folder."), 2539 | readLimit: z 2540 | .number() 2541 | .optional() 2542 | .describe("Number of files to ingest, optional. Defaults to 100."), 2543 | recurring: z 2544 | .boolean() 2545 | .optional() 2546 | .default(false) 2547 | .describe( 2548 | "Whether to create a recurring feed that checks for new content. Defaults to false (one-time execution). When true, isFeedDone is not needed." 2549 | ), 2550 | repeatInterval: z 2551 | .string() 2552 | .optional() 2553 | .default("PT15M") 2554 | .describe( 2555 | "ISO 8601 duration for recurring interval (e.g., 'PT5M' for 5 minutes, 'PT15M' for 15 minutes, 'PT1H' for 1 hour). Must be at least PT5M. Only used when recurring is true." 2556 | ), 2557 | }, 2558 | async ({ folderId, readLimit, recurring, repeatInterval }) => { 2559 | const client = new Graphlit(); 2560 | 2561 | try { 2562 | const clientId = process.env.BOX_CLIENT_ID; 2563 | if (!clientId) { 2564 | console.error("Please set BOX_CLIENT_ID environment variable."); 2565 | process.exit(1); 2566 | } 2567 | 2568 | const clientSecret = process.env.BOX_CLIENT_SECRET; 2569 | if (!clientSecret) { 2570 | console.error("Please set BOX_CLIENT_SECRET environment variable."); 2571 | process.exit(1); 2572 | } 2573 | 2574 | const redirectUri = process.env.BOX_REDIRECT_URI; 2575 | if (!redirectUri) { 2576 | console.error("Please set BOX_REDIRECT_URI environment variable."); 2577 | process.exit(1); 2578 | } 2579 | 2580 | const refreshToken = process.env.BOX_REFRESH_TOKEN; 2581 | if (!refreshToken) { 2582 | console.error("Please set BOX_REFRESH_TOKEN environment variable."); 2583 | process.exit(1); 2584 | } 2585 | 2586 | const response = await client.createFeed({ 2587 | name: `Box`, 2588 | type: FeedTypes.Site, 2589 | site: { 2590 | type: FeedServiceTypes.Box, 2591 | box: { 2592 | folderId: folderId, 2593 | clientId: clientId, 2594 | clientSecret: clientSecret, 2595 | redirectUri: redirectUri, 2596 | refreshToken: refreshToken, 2597 | }, 2598 | isRecursive: true, 2599 | readLimit: readLimit || 100, 2600 | }, 2601 | schedulePolicy: recurring 2602 | ? { 2603 | recurrenceType: TimedPolicyRecurrenceTypes.Repeat, 2604 | repeatInterval: repeatInterval || "PT15M", 2605 | } 2606 | : undefined, 2607 | }); 2608 | 2609 | return { 2610 | content: [ 2611 | { 2612 | type: "text", 2613 | text: JSON.stringify({ id: response.createFeed?.id }, null, 2), 2614 | }, 2615 | ], 2616 | }; 2617 | } catch (err: unknown) { 2618 | const error = err as Error; 2619 | return { 2620 | content: [ 2621 | { 2622 | type: "text", 2623 | text: `Error: ${error.message}`, 2624 | }, 2625 | ], 2626 | isError: true, 2627 | }; 2628 | } 2629 | } 2630 | ); 2631 | 2632 | server.tool( 2633 | "ingestGitHubFiles", 2634 | `Ingests files from GitHub repository into Graphlit knowledge base. 2635 | Accepts GitHub repository owner and repository name and an optional read limit for the number of files to ingest. 2636 | For example, for GitHub repository (https://github.com/openai/tiktoken), 'openai' is the repository owner, and 'tiktoken' is the repository name. 2637 | Requires environment variable to be configured: GITHUB_PERSONAL_ACCESS_TOKEN. 2638 | Executes asynchronously, creates GitHub feed, and returns the feed identifier. Optionally creates a recurring feed that checks for new content every 15 minutes when 'recurring' is set to true.`, 2639 | { 2640 | repositoryName: z.string().describe("GitHub repository name."), 2641 | repositoryOwner: z.string().describe("GitHub repository owner."), 2642 | readLimit: z 2643 | .number() 2644 | .optional() 2645 | .describe("Number of files to ingest, optional. Defaults to 100."), 2646 | recurring: z 2647 | .boolean() 2648 | .optional() 2649 | .default(false) 2650 | .describe( 2651 | "Whether to create a recurring feed that checks for new content. Defaults to false (one-time execution). When true, isFeedDone is not needed." 2652 | ), 2653 | repeatInterval: z 2654 | .string() 2655 | .optional() 2656 | .default("PT15M") 2657 | .describe( 2658 | "ISO 8601 duration for recurring interval (e.g., 'PT5M' for 5 minutes, 'PT15M' for 15 minutes, 'PT1H' for 1 hour). Must be at least PT5M. Only used when recurring is true." 2659 | ), 2660 | }, 2661 | async ({ 2662 | repositoryOwner, 2663 | repositoryName, 2664 | readLimit, 2665 | recurring, 2666 | repeatInterval, 2667 | }) => { 2668 | const client = new Graphlit(); 2669 | 2670 | try { 2671 | const personalAccessToken = process.env.GITHUB_PERSONAL_ACCESS_TOKEN; 2672 | if (!personalAccessToken) { 2673 | console.error( 2674 | "Please set GITHUB_PERSONAL_ACCESS_TOKEN environment variable." 2675 | ); 2676 | process.exit(1); 2677 | } 2678 | 2679 | const response = await client.createFeed({ 2680 | name: `GitHub`, 2681 | type: FeedTypes.Site, 2682 | site: { 2683 | type: FeedServiceTypes.GitHub, 2684 | github: { 2685 | repositoryOwner: repositoryOwner, 2686 | repositoryName: repositoryName, 2687 | personalAccessToken: personalAccessToken, 2688 | }, 2689 | isRecursive: true, 2690 | readLimit: readLimit || 100, 2691 | }, 2692 | schedulePolicy: recurring 2693 | ? { 2694 | recurrenceType: TimedPolicyRecurrenceTypes.Repeat, 2695 | repeatInterval: repeatInterval || "PT15M", 2696 | } 2697 | : undefined, 2698 | }); 2699 | 2700 | return { 2701 | content: [ 2702 | { 2703 | type: "text", 2704 | text: JSON.stringify({ id: response.createFeed?.id }, null, 2), 2705 | }, 2706 | ], 2707 | }; 2708 | } catch (err: unknown) { 2709 | const error = err as Error; 2710 | return { 2711 | content: [ 2712 | { 2713 | type: "text", 2714 | text: `Error: ${error.message}`, 2715 | }, 2716 | ], 2717 | isError: true, 2718 | }; 2719 | } 2720 | } 2721 | ); 2722 | 2723 | server.tool( 2724 | "ingestNotionPages", 2725 | `Ingests pages from Notion database into Graphlit knowledge base. 2726 | Accepts Notion database identifier and an optional read limit for the number of pages to ingest. 2727 | You can list the available Notion database identifiers with listNotionDatabases. 2728 | Or, for a Notion URL, https://www.notion.so/Example/Engineering-Wiki-114abc10cb38487e91ec906fc6c6f350, 'Engineering-Wiki-114abc10cb38487e91ec906fc6c6f350' is an example of a Notion database identifier. 2729 | Requires environment variable to be configured: NOTION_API_KEY. 2730 | Executes asynchronously, creates Notion feed, and returns the feed identifier. Optionally creates a recurring feed that checks for new content every 15 minutes when 'recurring' is set to true.`, 2731 | { 2732 | databaseId: z.string().describe("Notion database identifier."), 2733 | readLimit: z 2734 | .number() 2735 | .optional() 2736 | .describe("Number of pages to ingest, optional. Defaults to 100."), 2737 | recurring: z 2738 | .boolean() 2739 | .optional() 2740 | .default(false) 2741 | .describe( 2742 | "Whether to create a recurring feed that checks for new content. Defaults to false (one-time execution). When true, isFeedDone is not needed." 2743 | ), 2744 | repeatInterval: z 2745 | .string() 2746 | .optional() 2747 | .default("PT15M") 2748 | .describe( 2749 | "ISO 8601 duration for recurring interval (e.g., 'PT5M' for 5 minutes, 'PT15M' for 15 minutes, 'PT1H' for 1 hour). Must be at least PT5M. Only used when recurring is true." 2750 | ), 2751 | }, 2752 | async ({ databaseId, readLimit, recurring, repeatInterval }) => { 2753 | const client = new Graphlit(); 2754 | 2755 | try { 2756 | const token = process.env.NOTION_API_KEY; 2757 | if (!token) { 2758 | console.error("Please set NOTION_API_KEY environment variable."); 2759 | process.exit(1); 2760 | } 2761 | 2762 | const response = await client.createFeed({ 2763 | name: `Notion`, 2764 | type: FeedTypes.Notion, 2765 | notion: { 2766 | type: NotionTypes.Database, 2767 | identifiers: [databaseId], 2768 | token: token, 2769 | readLimit: readLimit || 100, 2770 | }, 2771 | schedulePolicy: recurring 2772 | ? { 2773 | recurrenceType: TimedPolicyRecurrenceTypes.Repeat, 2774 | repeatInterval: repeatInterval || "PT15M", 2775 | } 2776 | : undefined, 2777 | }); 2778 | 2779 | return { 2780 | content: [ 2781 | { 2782 | type: "text", 2783 | text: JSON.stringify({ id: response.createFeed?.id }, null, 2), 2784 | }, 2785 | ], 2786 | }; 2787 | } catch (err: unknown) { 2788 | const error = err as Error; 2789 | return { 2790 | content: [ 2791 | { 2792 | type: "text", 2793 | text: `Error: ${error.message}`, 2794 | }, 2795 | ], 2796 | isError: true, 2797 | }; 2798 | } 2799 | } 2800 | ); 2801 | 2802 | server.tool( 2803 | "ingestMicrosoftTeamsMessages", 2804 | `Ingests messages from Microsoft Teams channel into Graphlit knowledge base. 2805 | Accepts Microsoft Teams team identifier and channel identifier, and an optional read limit for the number of messages to ingest. 2806 | Requires environment variables to be configured: MICROSOFT_TEAMS_CLIENT_ID, MICROSOFT_TEAMS_CLIENT_SECRET, MICROSOFT_TEAMS_REFRESH_TOKEN. 2807 | Executes asynchronously, creates Microsoft Teams feed, and returns the feed identifier. Optionally creates a recurring feed that checks for new content every 15 minutes when 'recurring' is set to true.`, 2808 | { 2809 | teamId: z.string().describe("Microsoft Teams team identifier."), 2810 | channelId: z.string().describe("Microsoft Teams channel identifier."), 2811 | readLimit: z 2812 | .number() 2813 | .optional() 2814 | .describe("Number of messages to ingest, optional. Defaults to 100."), 2815 | recurring: z 2816 | .boolean() 2817 | .optional() 2818 | .default(false) 2819 | .describe( 2820 | "Whether to create a recurring feed that checks for new content. Defaults to false (one-time execution). When true, isFeedDone is not needed." 2821 | ), 2822 | repeatInterval: z 2823 | .string() 2824 | .optional() 2825 | .default("PT15M") 2826 | .describe( 2827 | "ISO 8601 duration for recurring interval (e.g., 'PT5M' for 5 minutes, 'PT15M' for 15 minutes, 'PT1H' for 1 hour). Must be at least PT5M. Only used when recurring is true." 2828 | ), 2829 | }, 2830 | async ({ teamId, channelId, readLimit, recurring, repeatInterval }) => { 2831 | const client = new Graphlit(); 2832 | 2833 | try { 2834 | const clientId = process.env.MICROSOFT_TEAMS_CLIENT_ID; 2835 | if (!clientId) { 2836 | console.error( 2837 | "Please set MICROSOFT_TEAMS_CLIENT_ID environment variable." 2838 | ); 2839 | process.exit(1); 2840 | } 2841 | 2842 | const clientSecret = process.env.MICROSOFT_TEAMS_CLIENT_SECRET; 2843 | if (!clientSecret) { 2844 | console.error( 2845 | "Please set MICROSOFT_TEAMS_CLIENT_SECRET environment variable." 2846 | ); 2847 | process.exit(1); 2848 | } 2849 | 2850 | const refreshToken = process.env.MICROSOFT_TEAMS_REFRESH_TOKEN; 2851 | if (!refreshToken) { 2852 | console.error( 2853 | "Please set MICROSOFT_TEAMS_REFRESH_TOKEN environment variable." 2854 | ); 2855 | process.exit(1); 2856 | } 2857 | 2858 | const response = await client.createFeed({ 2859 | name: `Microsoft Teams [${teamId}/${channelId}]`, 2860 | type: FeedTypes.MicrosoftTeams, 2861 | microsoftTeams: { 2862 | type: FeedListingTypes.Past, 2863 | clientId: clientId, 2864 | clientSecret: clientSecret, 2865 | refreshToken: refreshToken, 2866 | channelId: channelId, 2867 | teamId: teamId, 2868 | readLimit: readLimit || 100, 2869 | }, 2870 | schedulePolicy: recurring 2871 | ? { 2872 | recurrenceType: TimedPolicyRecurrenceTypes.Repeat, 2873 | repeatInterval: repeatInterval || "PT15M", 2874 | } 2875 | : undefined, 2876 | }); 2877 | 2878 | return { 2879 | content: [ 2880 | { 2881 | type: "text", 2882 | text: JSON.stringify({ id: response.createFeed?.id }, null, 2), 2883 | }, 2884 | ], 2885 | }; 2886 | } catch (err: unknown) { 2887 | const error = err as Error; 2888 | return { 2889 | content: [ 2890 | { 2891 | type: "text", 2892 | text: `Error: ${error.message}`, 2893 | }, 2894 | ], 2895 | isError: true, 2896 | }; 2897 | } 2898 | } 2899 | ); 2900 | 2901 | server.tool( 2902 | "ingestSlackMessages", 2903 | `Ingests messages from Slack channel into Graphlit knowledge base. 2904 | Accepts Slack channel name and an optional read limit for the number of messages to ingest. 2905 | Requires environment variable to be configured: SLACK_BOT_TOKEN. 2906 | Executes asynchronously, creates Slack feed, and returns the feed identifier. Optionally creates a recurring feed that checks for new content every 15 minutes when 'recurring' is set to true.`, 2907 | { 2908 | channelName: z.string().describe("Slack channel name."), 2909 | readLimit: z 2910 | .number() 2911 | .optional() 2912 | .describe("Number of messages to ingest, optional. Defaults to 100."), 2913 | recurring: z 2914 | .boolean() 2915 | .optional() 2916 | .default(false) 2917 | .describe( 2918 | "Whether to create a recurring feed that checks for new content. Defaults to false (one-time execution). When true, isFeedDone is not needed." 2919 | ), 2920 | repeatInterval: z 2921 | .string() 2922 | .optional() 2923 | .default("PT15M") 2924 | .describe( 2925 | "ISO 8601 duration for recurring interval (e.g., 'PT5M' for 5 minutes, 'PT15M' for 15 minutes, 'PT1H' for 1 hour). Must be at least PT5M. Only used when recurring is true." 2926 | ), 2927 | }, 2928 | async ({ channelName, readLimit, recurring, repeatInterval }) => { 2929 | const client = new Graphlit(); 2930 | 2931 | try { 2932 | const botToken = process.env.SLACK_BOT_TOKEN; 2933 | if (!botToken) { 2934 | console.error("Please set SLACK_BOT_TOKEN environment variable."); 2935 | process.exit(1); 2936 | } 2937 | 2938 | const response = await client.createFeed({ 2939 | name: `Slack [${channelName}]`, 2940 | type: FeedTypes.Slack, 2941 | slack: { 2942 | type: FeedListingTypes.Past, 2943 | channel: channelName, 2944 | token: botToken, 2945 | includeAttachments: true, 2946 | readLimit: readLimit || 100, 2947 | }, 2948 | schedulePolicy: recurring 2949 | ? { 2950 | recurrenceType: TimedPolicyRecurrenceTypes.Repeat, 2951 | repeatInterval: repeatInterval || "PT15M", 2952 | } 2953 | : undefined, 2954 | }); 2955 | 2956 | return { 2957 | content: [ 2958 | { 2959 | type: "text", 2960 | text: JSON.stringify({ id: response.createFeed?.id }, null, 2), 2961 | }, 2962 | ], 2963 | }; 2964 | } catch (err: unknown) { 2965 | const error = err as Error; 2966 | return { 2967 | content: [ 2968 | { 2969 | type: "text", 2970 | text: `Error: ${error.message}`, 2971 | }, 2972 | ], 2973 | isError: true, 2974 | }; 2975 | } 2976 | } 2977 | ); 2978 | 2979 | server.tool( 2980 | "ingestDiscordMessages", 2981 | `Ingests messages from Discord channel into Graphlit knowledge base. 2982 | Accepts Discord channel name and an optional read limit for the number of messages to ingest. 2983 | Requires environment variable to be configured: DISCORD_BOT_TOKEN. 2984 | Executes asynchronously, creates Discord feed, and returns the feed identifier. Optionally creates a recurring feed that checks for new content every 15 minutes when 'recurring' is set to true.`, 2985 | { 2986 | channelName: z.string().describe("Discord channel name."), 2987 | readLimit: z 2988 | .number() 2989 | .optional() 2990 | .describe("Number of messages to ingest, optional. Defaults to 100."), 2991 | recurring: z 2992 | .boolean() 2993 | .optional() 2994 | .default(false) 2995 | .describe( 2996 | "Whether to create a recurring feed that checks for new content. Defaults to false (one-time execution). When true, isFeedDone is not needed." 2997 | ), 2998 | repeatInterval: z 2999 | .string() 3000 | .optional() 3001 | .default("PT15M") 3002 | .describe( 3003 | "ISO 8601 duration for recurring interval (e.g., 'PT5M' for 5 minutes, 'PT15M' for 15 minutes, 'PT1H' for 1 hour). Must be at least PT5M. Only used when recurring is true." 3004 | ), 3005 | }, 3006 | async ({ channelName, readLimit, recurring, repeatInterval }) => { 3007 | const client = new Graphlit(); 3008 | 3009 | try { 3010 | const botToken = process.env.DISCORD_BOT_TOKEN; 3011 | if (!botToken) { 3012 | console.error("Please set DISCORD_BOT_TOKEN environment variable."); 3013 | process.exit(1); 3014 | } 3015 | 3016 | const response = await client.createFeed({ 3017 | name: `Discord [${channelName}]`, 3018 | type: FeedTypes.Discord, 3019 | discord: { 3020 | type: FeedListingTypes.Past, 3021 | channel: channelName, 3022 | token: botToken, 3023 | includeAttachments: true, 3024 | readLimit: readLimit || 100, 3025 | }, 3026 | schedulePolicy: recurring 3027 | ? { 3028 | recurrenceType: TimedPolicyRecurrenceTypes.Repeat, 3029 | repeatInterval: repeatInterval || "PT15M", 3030 | } 3031 | : undefined, 3032 | }); 3033 | 3034 | return { 3035 | content: [ 3036 | { 3037 | type: "text", 3038 | text: JSON.stringify({ id: response.createFeed?.id }, null, 2), 3039 | }, 3040 | ], 3041 | }; 3042 | } catch (err: unknown) { 3043 | const error = err as Error; 3044 | return { 3045 | content: [ 3046 | { 3047 | type: "text", 3048 | text: `Error: ${error.message}`, 3049 | }, 3050 | ], 3051 | isError: true, 3052 | }; 3053 | } 3054 | } 3055 | ); 3056 | 3057 | server.tool( 3058 | "ingestTwitterPosts", 3059 | `Ingests posts by user from Twitter/X into Graphlit knowledge base. 3060 | Accepts Twitter/X user name, without the leading @ symbol, and an optional read limit for the number of posts to ingest. 3061 | Requires environment variable to be configured: TWITTER_TOKEN. 3062 | Executes asynchronously, creates Twitter feed, and returns the feed identifier. Optionally creates a recurring feed that checks for new content every 15 minutes when 'recurring' is set to true.`, 3063 | { 3064 | userName: z 3065 | .string() 3066 | .describe( 3067 | "Twitter/X user name, without the leading @ symbol, i.e. 'graphlit'." 3068 | ), 3069 | readLimit: z 3070 | .number() 3071 | .optional() 3072 | .describe("Number of posts to ingest, optional. Defaults to 100."), 3073 | recurring: z 3074 | .boolean() 3075 | .optional() 3076 | .default(false) 3077 | .describe( 3078 | "Whether to create a recurring feed that checks for new content. Defaults to false (one-time execution). When true, isFeedDone is not needed." 3079 | ), 3080 | repeatInterval: z 3081 | .string() 3082 | .optional() 3083 | .default("PT15M") 3084 | .describe( 3085 | "ISO 8601 duration for recurring interval (e.g., 'PT5M' for 5 minutes, 'PT15M' for 15 minutes, 'PT1H' for 1 hour). Must be at least PT5M. Only used when recurring is true." 3086 | ), 3087 | }, 3088 | async ({ userName, readLimit, recurring, repeatInterval }) => { 3089 | const client = new Graphlit(); 3090 | 3091 | try { 3092 | const token = process.env.TWITTER_TOKEN; 3093 | if (!token) { 3094 | console.error("Please set TWITTER_TOKEN environment variable."); 3095 | process.exit(1); 3096 | } 3097 | 3098 | const response = await client.createFeed({ 3099 | name: `Twitter [${userName}]`, 3100 | type: FeedTypes.Twitter, 3101 | twitter: { 3102 | type: TwitterListingTypes.Posts, 3103 | userName: userName, 3104 | token: token, 3105 | includeAttachments: true, 3106 | readLimit: readLimit || 100, 3107 | }, 3108 | schedulePolicy: recurring 3109 | ? { 3110 | recurrenceType: TimedPolicyRecurrenceTypes.Repeat, 3111 | repeatInterval: repeatInterval || "PT15M", 3112 | } 3113 | : undefined, 3114 | }); 3115 | 3116 | return { 3117 | content: [ 3118 | { 3119 | type: "text", 3120 | text: JSON.stringify({ id: response.createFeed?.id }, null, 2), 3121 | }, 3122 | ], 3123 | }; 3124 | } catch (err: unknown) { 3125 | const error = err as Error; 3126 | return { 3127 | content: [ 3128 | { 3129 | type: "text", 3130 | text: `Error: ${error.message}`, 3131 | }, 3132 | ], 3133 | isError: true, 3134 | }; 3135 | } 3136 | } 3137 | ); 3138 | 3139 | server.tool( 3140 | "ingestTwitterSearch", 3141 | `Searches for recent posts from Twitter/X, and ingests them into Graphlit knowledge base. 3142 | Accepts search query, and an optional read limit for the number of posts to ingest. 3143 | Requires environment variable to be configured: TWITTER_TOKEN. 3144 | Executes asynchronously, creates Twitter feed, and returns the feed identifier. Optionally creates a recurring feed that checks for new content every 15 minutes when 'recurring' is set to true.`, 3145 | { 3146 | query: z.string().describe("Search query"), 3147 | readLimit: z 3148 | .number() 3149 | .optional() 3150 | .describe("Number of posts to ingest, optional. Defaults to 100."), 3151 | recurring: z 3152 | .boolean() 3153 | .optional() 3154 | .default(false) 3155 | .describe( 3156 | "Whether to create a recurring feed that checks for new content. Defaults to false (one-time execution). When true, isFeedDone is not needed." 3157 | ), 3158 | repeatInterval: z 3159 | .string() 3160 | .optional() 3161 | .default("PT15M") 3162 | .describe( 3163 | "ISO 8601 duration for recurring interval (e.g., 'PT5M' for 5 minutes, 'PT15M' for 15 minutes, 'PT1H' for 1 hour). Must be at least PT5M. Only used when recurring is true." 3164 | ), 3165 | }, 3166 | async ({ query, readLimit, recurring, repeatInterval }) => { 3167 | const client = new Graphlit(); 3168 | 3169 | try { 3170 | const token = process.env.TWITTER_TOKEN; 3171 | if (!token) { 3172 | console.error("Please set TWITTER_TOKEN environment variable."); 3173 | process.exit(1); 3174 | } 3175 | 3176 | const response = await client.createFeed({ 3177 | name: `Twitter [${query}]`, 3178 | type: FeedTypes.Twitter, 3179 | twitter: { 3180 | type: TwitterListingTypes.RecentSearch, 3181 | query: query, 3182 | token: token, 3183 | includeAttachments: true, 3184 | readLimit: readLimit || 100, 3185 | }, 3186 | schedulePolicy: recurring 3187 | ? { 3188 | recurrenceType: TimedPolicyRecurrenceTypes.Repeat, 3189 | repeatInterval: repeatInterval || "PT15M", 3190 | } 3191 | : undefined, 3192 | }); 3193 | 3194 | return { 3195 | content: [ 3196 | { 3197 | type: "text", 3198 | text: JSON.stringify({ id: response.createFeed?.id }, null, 2), 3199 | }, 3200 | ], 3201 | }; 3202 | } catch (err: unknown) { 3203 | const error = err as Error; 3204 | return { 3205 | content: [ 3206 | { 3207 | type: "text", 3208 | text: `Error: ${error.message}`, 3209 | }, 3210 | ], 3211 | isError: true, 3212 | }; 3213 | } 3214 | } 3215 | ); 3216 | 3217 | server.tool( 3218 | "ingestRedditPosts", 3219 | `Ingests posts from Reddit subreddit into Graphlit knowledge base. 3220 | Accepts a subreddit name and an optional read limit for the number of posts to ingest. 3221 | Executes asynchronously, creates Reddit feed, and returns the feed identifier. Optionally creates a recurring feed that checks for new content every 15 minutes when 'recurring' is set to true.`, 3222 | { 3223 | subredditName: z.string().describe("Subreddit name."), 3224 | readLimit: z 3225 | .number() 3226 | .optional() 3227 | .describe("Number of posts to ingest, optional. Defaults to 100."), 3228 | recurring: z 3229 | .boolean() 3230 | .optional() 3231 | .default(false) 3232 | .describe( 3233 | "Whether to create a recurring feed that checks for new content. Defaults to false (one-time execution). When true, isFeedDone is not needed." 3234 | ), 3235 | repeatInterval: z 3236 | .string() 3237 | .optional() 3238 | .default("PT15M") 3239 | .describe( 3240 | "ISO 8601 duration for recurring interval (e.g., 'PT5M' for 5 minutes, 'PT15M' for 15 minutes, 'PT1H' for 1 hour). Must be at least PT5M. Only used when recurring is true." 3241 | ), 3242 | }, 3243 | async ({ subredditName, readLimit, recurring, repeatInterval }) => { 3244 | const client = new Graphlit(); 3245 | 3246 | try { 3247 | const response = await client.createFeed({ 3248 | name: `Reddit [${subredditName}]`, 3249 | type: FeedTypes.Reddit, 3250 | reddit: { 3251 | subredditName: subredditName, 3252 | readLimit: readLimit || 100, 3253 | }, 3254 | schedulePolicy: recurring 3255 | ? { 3256 | recurrenceType: TimedPolicyRecurrenceTypes.Repeat, 3257 | repeatInterval: repeatInterval || "PT15M", 3258 | } 3259 | : undefined, 3260 | }); 3261 | 3262 | return { 3263 | content: [ 3264 | { 3265 | type: "text", 3266 | text: JSON.stringify({ id: response.createFeed?.id }, null, 2), 3267 | }, 3268 | ], 3269 | }; 3270 | } catch (err: unknown) { 3271 | const error = err as Error; 3272 | return { 3273 | content: [ 3274 | { 3275 | type: "text", 3276 | text: `Error: ${error.message}`, 3277 | }, 3278 | ], 3279 | isError: true, 3280 | }; 3281 | } 3282 | } 3283 | ); 3284 | 3285 | server.tool( 3286 | "ingestGoogleEmail", 3287 | `Ingests emails from Google Email account into Graphlit knowledge base. 3288 | Accepts an optional read limit for the number of emails to ingest. 3289 | Requires environment variables to be configured: GOOGLE_EMAIL_CLIENT_ID, GOOGLE_EMAIL_CLIENT_SECRET, GOOGLE_EMAIL_REFRESH_TOKEN. 3290 | Executes asynchronously, creates Google Email feed, and returns the feed identifier. Optionally creates a recurring feed that checks for new content every 15 minutes when 'recurring' is set to true.`, 3291 | { 3292 | readLimit: z 3293 | .number() 3294 | .optional() 3295 | .describe("Number of emails to ingest, optional. Defaults to 100."), 3296 | recurring: z 3297 | .boolean() 3298 | .optional() 3299 | .default(false) 3300 | .describe( 3301 | "Whether to create a recurring feed that checks for new content. Defaults to false (one-time execution). When true, isFeedDone is not needed." 3302 | ), 3303 | repeatInterval: z 3304 | .string() 3305 | .optional() 3306 | .default("PT15M") 3307 | .describe( 3308 | "ISO 8601 duration for recurring interval (e.g., 'PT5M' for 5 minutes, 'PT15M' for 15 minutes, 'PT1H' for 1 hour). Must be at least PT5M. Only used when recurring is true." 3309 | ), 3310 | }, 3311 | async ({ readLimit, recurring, repeatInterval }) => { 3312 | const client = new Graphlit(); 3313 | 3314 | try { 3315 | const clientId = process.env.GOOGLE_EMAIL_CLIENT_ID; 3316 | if (!clientId) { 3317 | console.error( 3318 | "Please set GOOGLE_EMAIL_CLIENT_ID environment variable." 3319 | ); 3320 | process.exit(1); 3321 | } 3322 | 3323 | const clientSecret = process.env.GOOGLE_EMAIL_CLIENT_SECRET; 3324 | if (!clientSecret) { 3325 | console.error( 3326 | "Please set GOOGLE_EMAIL_CLIENT_SECRET environment variable." 3327 | ); 3328 | process.exit(1); 3329 | } 3330 | 3331 | const refreshToken = process.env.GOOGLE_EMAIL_REFRESH_TOKEN; 3332 | if (!refreshToken) { 3333 | console.error( 3334 | "Please set GOOGLE_EMAIL_REFRESH_TOKEN environment variable." 3335 | ); 3336 | process.exit(1); 3337 | } 3338 | 3339 | const response = await client.createFeed({ 3340 | name: `Google Email`, 3341 | type: FeedTypes.Email, 3342 | email: { 3343 | type: FeedServiceTypes.GoogleEmail, 3344 | google: { 3345 | type: EmailListingTypes.Past, 3346 | refreshToken: refreshToken, 3347 | clientId: clientId, 3348 | clientSecret: clientSecret, 3349 | }, 3350 | includeAttachments: true, 3351 | readLimit: readLimit || 100, 3352 | }, 3353 | schedulePolicy: recurring 3354 | ? { 3355 | recurrenceType: TimedPolicyRecurrenceTypes.Repeat, 3356 | repeatInterval: repeatInterval || "PT15M", 3357 | } 3358 | : undefined, 3359 | }); 3360 | 3361 | return { 3362 | content: [ 3363 | { 3364 | type: "text", 3365 | text: JSON.stringify({ id: response.createFeed?.id }, null, 2), 3366 | }, 3367 | ], 3368 | }; 3369 | } catch (err: unknown) { 3370 | const error = err as Error; 3371 | return { 3372 | content: [ 3373 | { 3374 | type: "text", 3375 | text: `Error: ${error.message}`, 3376 | }, 3377 | ], 3378 | isError: true, 3379 | }; 3380 | } 3381 | } 3382 | ); 3383 | 3384 | server.tool( 3385 | "ingestMicrosoftEmail", 3386 | `Ingests emails from Microsoft Email account into Graphlit knowledge base. 3387 | Accepts an optional read limit for the number of emails to ingest. 3388 | Requires environment variables to be configured: MICROSOFT_EMAIL_CLIENT_ID, MICROSOFT_EMAIL_CLIENT_SECRET, MICROSOFT_EMAIL_REFRESH_TOKEN. 3389 | Executes asynchronously, creates Microsoft Email feed, and returns the feed identifier. Optionally creates a recurring feed that checks for new content every 15 minutes when 'recurring' is set to true.`, 3390 | { 3391 | readLimit: z 3392 | .number() 3393 | .optional() 3394 | .describe("Number of emails to ingest, optional. Defaults to 100."), 3395 | recurring: z 3396 | .boolean() 3397 | .optional() 3398 | .default(false) 3399 | .describe( 3400 | "Whether to create a recurring feed that checks for new content. Defaults to false (one-time execution). When true, isFeedDone is not needed." 3401 | ), 3402 | repeatInterval: z 3403 | .string() 3404 | .optional() 3405 | .default("PT15M") 3406 | .describe( 3407 | "ISO 8601 duration for recurring interval (e.g., 'PT5M' for 5 minutes, 'PT15M' for 15 minutes, 'PT1H' for 1 hour). Must be at least PT5M. Only used when recurring is true." 3408 | ), 3409 | }, 3410 | async ({ readLimit, recurring, repeatInterval }) => { 3411 | const client = new Graphlit(); 3412 | 3413 | try { 3414 | const clientId = process.env.MICROSOFT_EMAIL_CLIENT_ID; 3415 | if (!clientId) { 3416 | console.error( 3417 | "Please set MICROSOFT_EMAIL_CLIENT_ID environment variable." 3418 | ); 3419 | process.exit(1); 3420 | } 3421 | 3422 | const clientSecret = process.env.MICROSOFT_EMAIL_CLIENT_SECRET; 3423 | if (!clientSecret) { 3424 | console.error( 3425 | "Please set MICROSOFT_EMAIL_CLIENT_SECRET environment variable." 3426 | ); 3427 | process.exit(1); 3428 | } 3429 | 3430 | const refreshToken = process.env.MICROSOFT_EMAIL_REFRESH_TOKEN; 3431 | if (!refreshToken) { 3432 | console.error( 3433 | "Please set MICROSOFT_EMAIL_REFRESH_TOKEN environment variable." 3434 | ); 3435 | process.exit(1); 3436 | } 3437 | 3438 | const response = await client.createFeed({ 3439 | name: `Microsoft Email`, 3440 | type: FeedTypes.Email, 3441 | email: { 3442 | type: FeedServiceTypes.MicrosoftEmail, 3443 | microsoft: { 3444 | type: EmailListingTypes.Past, 3445 | refreshToken: refreshToken, 3446 | clientId: clientId, 3447 | clientSecret: clientSecret, 3448 | }, 3449 | includeAttachments: true, 3450 | readLimit: readLimit || 100, 3451 | }, 3452 | schedulePolicy: recurring 3453 | ? { 3454 | recurrenceType: TimedPolicyRecurrenceTypes.Repeat, 3455 | repeatInterval: repeatInterval || "PT15M", 3456 | } 3457 | : undefined, 3458 | }); 3459 | 3460 | return { 3461 | content: [ 3462 | { 3463 | type: "text", 3464 | text: JSON.stringify({ id: response.createFeed?.id }, null, 2), 3465 | }, 3466 | ], 3467 | }; 3468 | } catch (err: unknown) { 3469 | const error = err as Error; 3470 | return { 3471 | content: [ 3472 | { 3473 | type: "text", 3474 | text: `Error: ${error.message}`, 3475 | }, 3476 | ], 3477 | isError: true, 3478 | }; 3479 | } 3480 | } 3481 | ); 3482 | 3483 | server.tool( 3484 | "ingestLinearIssues", 3485 | `Ingests issues from Linear project into Graphlit knowledge base. 3486 | Accepts Linear project name and an optional read limit for the number of issues to ingest. 3487 | Requires environment variable to be configured: LINEAR_API_KEY. 3488 | Executes asynchronously, creates Linear issue feed, and returns the feed identifier. Optionally creates a recurring feed that checks for new content every 15 minutes when 'recurring' is set to true.`, 3489 | { 3490 | projectName: z.string().describe("Linear project name."), 3491 | readLimit: z 3492 | .number() 3493 | .optional() 3494 | .describe("Number of issues to ingest, optional. Defaults to 100."), 3495 | recurring: z 3496 | .boolean() 3497 | .optional() 3498 | .default(false) 3499 | .describe( 3500 | "Whether to create a recurring feed that checks for new content. Defaults to false (one-time execution). When true, isFeedDone is not needed." 3501 | ), 3502 | repeatInterval: z 3503 | .string() 3504 | .optional() 3505 | .default("PT15M") 3506 | .describe( 3507 | "ISO 8601 duration for recurring interval (e.g., 'PT5M' for 5 minutes, 'PT15M' for 15 minutes, 'PT1H' for 1 hour). Must be at least PT5M. Only used when recurring is true." 3508 | ), 3509 | }, 3510 | async ({ projectName, readLimit, recurring, repeatInterval }) => { 3511 | const client = new Graphlit(); 3512 | 3513 | try { 3514 | const apiKey = process.env.LINEAR_API_KEY; 3515 | if (!apiKey) { 3516 | console.error("Please set LINEAR_API_KEY environment variable."); 3517 | process.exit(1); 3518 | } 3519 | 3520 | const response = await client.createFeed({ 3521 | name: `Linear [${projectName}]`, 3522 | type: FeedTypes.Issue, 3523 | issue: { 3524 | type: FeedServiceTypes.Linear, 3525 | linear: { 3526 | project: projectName, 3527 | key: apiKey, 3528 | }, 3529 | includeAttachments: true, 3530 | readLimit: readLimit || 100, 3531 | }, 3532 | schedulePolicy: recurring 3533 | ? { 3534 | recurrenceType: TimedPolicyRecurrenceTypes.Repeat, 3535 | repeatInterval: repeatInterval || "PT15M", 3536 | } 3537 | : undefined, 3538 | }); 3539 | 3540 | return { 3541 | content: [ 3542 | { 3543 | type: "text", 3544 | text: JSON.stringify({ id: response.createFeed?.id }, null, 2), 3545 | }, 3546 | ], 3547 | }; 3548 | } catch (err: unknown) { 3549 | const error = err as Error; 3550 | return { 3551 | content: [ 3552 | { 3553 | type: "text", 3554 | text: `Error: ${error.message}`, 3555 | }, 3556 | ], 3557 | isError: true, 3558 | }; 3559 | } 3560 | } 3561 | ); 3562 | 3563 | server.tool( 3564 | "ingestGitHubIssues", 3565 | `Ingests issues from GitHub repository into Graphlit knowledge base. 3566 | Accepts GitHub repository owner and repository name and an optional read limit for the number of issues to ingest. 3567 | For example, for GitHub repository (https://github.com/openai/tiktoken), 'openai' is the repository owner, and 'tiktoken' is the repository name. 3568 | Requires environment variable to be configured: GITHUB_PERSONAL_ACCESS_TOKEN. 3569 | Executes asynchronously, creates GitHub issue feed, and returns the feed identifier. Optionally creates a recurring feed that checks for new content every 15 minutes when 'recurring' is set to true.`, 3570 | { 3571 | repositoryName: z.string().describe("GitHub repository name."), 3572 | repositoryOwner: z.string().describe("GitHub repository owner."), 3573 | readLimit: z 3574 | .number() 3575 | .optional() 3576 | .describe("Number of issues to ingest, optional. Defaults to 100."), 3577 | recurring: z 3578 | .boolean() 3579 | .optional() 3580 | .default(false) 3581 | .describe( 3582 | "Whether to create a recurring feed that checks for new content. Defaults to false (one-time execution). When true, isFeedDone is not needed." 3583 | ), 3584 | repeatInterval: z 3585 | .string() 3586 | .optional() 3587 | .default("PT15M") 3588 | .describe( 3589 | "ISO 8601 duration for recurring interval (e.g., 'PT5M' for 5 minutes, 'PT15M' for 15 minutes, 'PT1H' for 1 hour). Must be at least PT5M. Only used when recurring is true." 3590 | ), 3591 | }, 3592 | async ({ 3593 | repositoryName, 3594 | repositoryOwner, 3595 | readLimit, 3596 | recurring, 3597 | repeatInterval, 3598 | }) => { 3599 | const client = new Graphlit(); 3600 | 3601 | try { 3602 | const personalAccessToken = process.env.GITHUB_PERSONAL_ACCESS_TOKEN; 3603 | if (!personalAccessToken) { 3604 | console.error( 3605 | "Please set GITHUB_PERSONAL_ACCESS_TOKEN environment variable." 3606 | ); 3607 | process.exit(1); 3608 | } 3609 | 3610 | const response = await client.createFeed({ 3611 | name: `GitHub [${repositoryOwner}/${repositoryName}]`, 3612 | type: FeedTypes.Issue, 3613 | issue: { 3614 | type: FeedServiceTypes.GitHubIssues, 3615 | github: { 3616 | repositoryName: repositoryName, 3617 | repositoryOwner: repositoryOwner, 3618 | personalAccessToken: personalAccessToken, 3619 | }, 3620 | includeAttachments: true, 3621 | readLimit: readLimit || 100, 3622 | }, 3623 | schedulePolicy: recurring 3624 | ? { 3625 | recurrenceType: TimedPolicyRecurrenceTypes.Repeat, 3626 | repeatInterval: repeatInterval || "PT15M", 3627 | } 3628 | : undefined, 3629 | }); 3630 | 3631 | return { 3632 | content: [ 3633 | { 3634 | type: "text", 3635 | text: JSON.stringify({ id: response.createFeed?.id }, null, 2), 3636 | }, 3637 | ], 3638 | }; 3639 | } catch (err: unknown) { 3640 | const error = err as Error; 3641 | return { 3642 | content: [ 3643 | { 3644 | type: "text", 3645 | text: `Error: ${error.message}`, 3646 | }, 3647 | ], 3648 | isError: true, 3649 | }; 3650 | } 3651 | } 3652 | ); 3653 | 3654 | server.tool( 3655 | "ingestJiraIssues", 3656 | `Ingests issues from Atlassian Jira repository into Graphlit knowledge base. 3657 | Accepts Atlassian Jira server URL and project name, and an optional read limit for the number of issues to ingest. 3658 | Requires environment variables to be configured: JIRA_EMAIL, JIRA_TOKEN. 3659 | Executes asynchronously, creates Atlassian Jira issue feed, and returns the feed identifier. Optionally creates a recurring feed that checks for new content every 15 minutes when 'recurring' is set to true.`, 3660 | { 3661 | url: z.string().describe("Atlassian Jira server URL."), 3662 | projectName: z.string().describe("Atlassian Jira project name."), 3663 | readLimit: z 3664 | .number() 3665 | .optional() 3666 | .describe("Number of issues to ingest, optional. Defaults to 100."), 3667 | recurring: z 3668 | .boolean() 3669 | .optional() 3670 | .default(false) 3671 | .describe( 3672 | "Whether to create a recurring feed that checks for new content. Defaults to false (one-time execution). When true, isFeedDone is not needed." 3673 | ), 3674 | repeatInterval: z 3675 | .string() 3676 | .optional() 3677 | .default("PT15M") 3678 | .describe( 3679 | "ISO 8601 duration for recurring interval (e.g., 'PT5M' for 5 minutes, 'PT15M' for 15 minutes, 'PT1H' for 1 hour). Must be at least PT5M. Only used when recurring is true." 3680 | ), 3681 | }, 3682 | async ({ url, projectName, readLimit, recurring, repeatInterval }) => { 3683 | const client = new Graphlit(); 3684 | 3685 | try { 3686 | const email = process.env.JIRA_EMAIL; 3687 | if (!email) { 3688 | console.error("Please set JIRA_EMAIL environment variable."); 3689 | process.exit(1); 3690 | } 3691 | 3692 | const token = process.env.JIRA_TOKEN; 3693 | if (!token) { 3694 | console.error("Please set JIRA_TOKEN environment variable."); 3695 | process.exit(1); 3696 | } 3697 | 3698 | const response = await client.createFeed({ 3699 | name: `Jira [${projectName}]`, 3700 | type: FeedTypes.Issue, 3701 | issue: { 3702 | type: FeedServiceTypes.AtlassianJira, 3703 | jira: { 3704 | uri: url, 3705 | project: projectName, 3706 | email: email, 3707 | token: token, 3708 | }, 3709 | includeAttachments: true, 3710 | readLimit: readLimit || 100, 3711 | }, 3712 | schedulePolicy: recurring 3713 | ? { 3714 | recurrenceType: TimedPolicyRecurrenceTypes.Repeat, 3715 | repeatInterval: repeatInterval || "PT15M", 3716 | } 3717 | : undefined, 3718 | }); 3719 | 3720 | return { 3721 | content: [ 3722 | { 3723 | type: "text", 3724 | text: JSON.stringify({ id: response.createFeed?.id }, null, 2), 3725 | }, 3726 | ], 3727 | }; 3728 | } catch (err: unknown) { 3729 | const error = err as Error; 3730 | return { 3731 | content: [ 3732 | { 3733 | type: "text", 3734 | text: `Error: ${error.message}`, 3735 | }, 3736 | ], 3737 | isError: true, 3738 | }; 3739 | } 3740 | } 3741 | ); 3742 | 3743 | server.tool( 3744 | "webCrawl", 3745 | `Crawls web pages from web site into Graphlit knowledge base. 3746 | Accepts a URL and an optional read limit for the number of pages to crawl. 3747 | Uses sitemap.xml to discover pages to be crawled from website. 3748 | Executes asynchronously and returns the feed identifier. Optionally creates a recurring feed that checks for new content every 15 minutes when 'recurring' is set to true.`, 3749 | { 3750 | url: z.string().describe("Web site URL."), 3751 | readLimit: z 3752 | .number() 3753 | .optional() 3754 | .describe("Number of web pages to ingest, optional. Defaults to 100."), 3755 | recurring: z 3756 | .boolean() 3757 | .optional() 3758 | .default(false) 3759 | .describe( 3760 | "Whether to create a recurring feed that checks for new content. Defaults to false (one-time execution). When true, isFeedDone is not needed." 3761 | ), 3762 | repeatInterval: z 3763 | .string() 3764 | .optional() 3765 | .default("PT15M") 3766 | .describe( 3767 | "ISO 8601 duration for recurring interval (e.g., 'PT5M' for 5 minutes, 'PT15M' for 15 minutes, 'PT1H' for 1 hour). Must be at least PT5M. Only used when recurring is true." 3768 | ), 3769 | }, 3770 | async ({ url, readLimit, recurring, repeatInterval }) => { 3771 | const client = new Graphlit(); 3772 | 3773 | try { 3774 | const response = await client.createFeed({ 3775 | name: `Web [${url}]`, 3776 | type: FeedTypes.Web, 3777 | web: { 3778 | uri: url, 3779 | readLimit: readLimit || 100, 3780 | }, 3781 | schedulePolicy: recurring 3782 | ? { 3783 | recurrenceType: TimedPolicyRecurrenceTypes.Repeat, 3784 | repeatInterval: repeatInterval || "PT15M", 3785 | } 3786 | : undefined, 3787 | }); 3788 | 3789 | return { 3790 | content: [ 3791 | { 3792 | type: "text", 3793 | text: JSON.stringify({ id: response.createFeed?.id }, null, 2), 3794 | }, 3795 | ], 3796 | }; 3797 | } catch (err: unknown) { 3798 | const error = err as Error; 3799 | return { 3800 | content: [ 3801 | { 3802 | type: "text", 3803 | text: `Error: ${error.message}`, 3804 | }, 3805 | ], 3806 | isError: true, 3807 | }; 3808 | } 3809 | } 3810 | ); 3811 | 3812 | server.tool( 3813 | "webMap", 3814 | `Enumerates the web pages at or beneath the provided URL using web sitemap. 3815 | Does *not* ingest web pages into Graphlit knowledge base. 3816 | Accepts web site URL as string. 3817 | Returns list of mapped URIs from web site.`, 3818 | { 3819 | url: z.string().describe("Web site URL."), 3820 | }, 3821 | async ({ url }) => { 3822 | const client = new Graphlit(); 3823 | 3824 | try { 3825 | const response = await client.mapWeb(url); 3826 | 3827 | return { 3828 | content: [ 3829 | { 3830 | type: "text", 3831 | text: JSON.stringify(response.mapWeb?.results, null, 2), 3832 | }, 3833 | ], 3834 | }; 3835 | } catch (err: unknown) { 3836 | const error = err as Error; 3837 | return { 3838 | content: [ 3839 | { 3840 | type: "text", 3841 | text: `Error: ${error.message}`, 3842 | }, 3843 | ], 3844 | isError: true, 3845 | }; 3846 | } 3847 | } 3848 | ); 3849 | 3850 | server.tool( 3851 | "webSearch", 3852 | `Performs web or podcast search based on search query. Can search for web pages or anything about podcasts (i.e. episodes, topics, guest appearances). 3853 | Format the search query as what would be entered into a Google search. You can use site filtering in the search query, like 'site:twitter.com'. 3854 | Accepts search query as string, and optional search service type. 3855 | Prefer calling this tool over using 'curl' directly for any web search. 3856 | Use 'PODSCAN' search service type to search podcasts. 3857 | Does *not* ingest pages or podcast episodes into Graphlit knowledge base. 3858 | When searching podcasts, *don't* include the term 'podcast' or 'episode' in the search query - that would be redundant. 3859 | Search service types: Tavily (web pages), Exa (web pages) and Podscan (podcasts). Defaults to Exa. 3860 | Returns URL, title and relevant Markdown text from resulting web pages or podcast episode descriptions.`, 3861 | { 3862 | query: z.string().describe("Search query."), 3863 | searchService: z 3864 | .nativeEnum(SearchServiceTypes) 3865 | .optional() 3866 | .default(SearchServiceTypes.Exa) 3867 | .describe( 3868 | "Search service type (Tavily, Exa, Podscan). Defaults to Exa." 3869 | ), 3870 | limit: z 3871 | .number() 3872 | .optional() 3873 | .default(10) 3874 | .describe( 3875 | "Limit the number of search hits to be returned. Defaults to 10." 3876 | ), 3877 | }, 3878 | async ({ query, searchService, limit }) => { 3879 | const client = new Graphlit(); 3880 | 3881 | try { 3882 | const response = await client.searchWeb(query, searchService, limit); 3883 | 3884 | return { 3885 | content: [ 3886 | { 3887 | type: "text", 3888 | text: JSON.stringify(response.searchWeb?.results, null, 2), 3889 | }, 3890 | ], 3891 | }; 3892 | } catch (err: unknown) { 3893 | const error = err as Error; 3894 | return { 3895 | content: [ 3896 | { 3897 | type: "text", 3898 | text: `Error: ${error.message}`, 3899 | }, 3900 | ], 3901 | isError: true, 3902 | }; 3903 | } 3904 | } 3905 | ); 3906 | 3907 | server.tool( 3908 | "ingestRSS", 3909 | `Ingests posts from RSS feed into Graphlit knowledge base. 3910 | For podcast RSS feeds, audio will be downloaded, transcribed and ingested into Graphlit knowledge base. 3911 | Accepts RSS URL and an optional read limit for the number of posts to read. 3912 | Executes asynchronously and returns the feed identifier. Optionally creates a recurring feed that checks for new content every 15 minutes when 'recurring' is set to true.`, 3913 | { 3914 | url: z.string().describe("RSS URL."), 3915 | readLimit: z 3916 | .number() 3917 | .optional() 3918 | .describe("Number of issues to posts, optional. Defaults to 25."), 3919 | recurring: z 3920 | .boolean() 3921 | .optional() 3922 | .default(false) 3923 | .describe( 3924 | "Whether to create a recurring feed that checks for new content. Defaults to false (one-time execution). When true, isFeedDone is not needed." 3925 | ), 3926 | repeatInterval: z 3927 | .string() 3928 | .optional() 3929 | .default("PT15M") 3930 | .describe( 3931 | "ISO 8601 duration for recurring interval (e.g., 'PT5M' for 5 minutes, 'PT15M' for 15 minutes, 'PT1H' for 1 hour). Must be at least PT5M. Only used when recurring is true." 3932 | ), 3933 | }, 3934 | async ({ url, readLimit, recurring, repeatInterval }) => { 3935 | const client = new Graphlit(); 3936 | 3937 | try { 3938 | const response = await client.createFeed({ 3939 | name: `RSS [${url}]`, 3940 | type: FeedTypes.Rss, 3941 | rss: { 3942 | uri: url, 3943 | readLimit: readLimit || 25, 3944 | }, 3945 | schedulePolicy: recurring 3946 | ? { 3947 | recurrenceType: TimedPolicyRecurrenceTypes.Repeat, 3948 | repeatInterval: repeatInterval || "PT15M", 3949 | } 3950 | : undefined, 3951 | }); 3952 | 3953 | return { 3954 | content: [ 3955 | { 3956 | type: "text", 3957 | text: JSON.stringify({ id: response.createFeed?.id }, null, 2), 3958 | }, 3959 | ], 3960 | }; 3961 | } catch (err: unknown) { 3962 | const error = err as Error; 3963 | return { 3964 | content: [ 3965 | { 3966 | type: "text", 3967 | text: `Error: ${error.message}`, 3968 | }, 3969 | ], 3970 | isError: true, 3971 | }; 3972 | } 3973 | } 3974 | ); 3975 | 3976 | server.tool( 3977 | "ingestUrl", 3978 | `Ingests content from URL into Graphlit knowledge base. 3979 | Can scrape a single web page, and can ingest individual Word documents, PDFs, audio recordings, videos, images, or any other unstructured data. 3980 | Do *not* use for crawling a web site, which is done with 'webCrawl' tool. 3981 | Executes asynchronously and returns the content identifier.`, 3982 | { 3983 | url: z.string().describe("URL to ingest content from."), 3984 | }, 3985 | async ({ url }) => { 3986 | const client = new Graphlit(); 3987 | 3988 | try { 3989 | const response = await client.ingestUri(url); 3990 | 3991 | return { 3992 | content: [ 3993 | { 3994 | type: "text", 3995 | text: JSON.stringify({ id: response.ingestUri?.id }, null, 2), 3996 | }, 3997 | ], 3998 | }; 3999 | } catch (err: unknown) { 4000 | const error = err as Error; 4001 | return { 4002 | content: [ 4003 | { 4004 | type: "text", 4005 | text: `Error: ${error.message}`, 4006 | }, 4007 | ], 4008 | isError: true, 4009 | }; 4010 | } 4011 | } 4012 | ); 4013 | 4014 | server.tool( 4015 | "ingestText", 4016 | `Ingests text as content into Graphlit knowledge base. 4017 | Accepts the text itself, and an optional text type (Plain, Markdown, Html). Defaults to Markdown text type. 4018 | Optionally accepts the content name and an identifier for an existing content object. Will overwrite existing content, if provided. 4019 | Can use for storing the output from LLM or other tools as content resources, which can be later searched or retrieved. 4020 | Executes *synchronously* and returns the content identifier.`, 4021 | { 4022 | name: z 4023 | .string() 4024 | .optional() 4025 | .describe("Name for the content object, optional."), 4026 | text: z.string().describe("Text content to ingest."), 4027 | textType: z 4028 | .nativeEnum(TextTypes) 4029 | .optional() 4030 | .default(TextTypes.Markdown) 4031 | .describe("Text type (Plain, Markdown, Html). Defaults to Markdown."), 4032 | id: z 4033 | .string() 4034 | .optional() 4035 | .describe( 4036 | "Optional content identifier. Will overwrite existing content, if provided." 4037 | ), 4038 | }, 4039 | async ({ name, text, textType, id }) => { 4040 | const client = new Graphlit(); 4041 | 4042 | try { 4043 | const response = await client.ingestText( 4044 | text, 4045 | name, 4046 | textType, 4047 | undefined, 4048 | id, 4049 | true 4050 | ); 4051 | 4052 | return { 4053 | content: [ 4054 | { 4055 | type: "text", 4056 | text: JSON.stringify({ id: response.ingestText?.id }, null, 2), 4057 | }, 4058 | ], 4059 | }; 4060 | } catch (err: unknown) { 4061 | const error = err as Error; 4062 | return { 4063 | content: [ 4064 | { 4065 | type: "text", 4066 | text: `Error: ${error.message}`, 4067 | }, 4068 | ], 4069 | isError: true, 4070 | }; 4071 | } 4072 | } 4073 | ); 4074 | 4075 | server.tool( 4076 | "ingestMemory", 4077 | `Ingests short-term textual memory as content into Graphlit knowledge base. 4078 | Accepts an optional text type (Plain, Markdown, Html). Defaults to Markdown text type. Optionally accepts the content name. 4079 | Will automatically be entity extracted into a knowledge graph. 4080 | Use for storing short-term memories about the user or agent, which can be later searched or retrieved. Memories are transient and will be deleted after a period of time. 4081 | Can use 'queryContents' or 'retrieveSources' tools to search for memories, by specifying the 'MEMORY' content type. 4082 | Executes asynchronously and returns the content identifier.`, 4083 | { 4084 | name: z.string().optional().describe("Name for the content object."), 4085 | text: z 4086 | .string() 4087 | .describe( 4088 | "Textual memory to ingest, i.e. 'Kirk likes raccoons' or 'Graphlit is based in Seattle'" 4089 | ), 4090 | textType: z 4091 | .nativeEnum(TextTypes) 4092 | .optional() 4093 | .default(TextTypes.Markdown) 4094 | .describe("Text type (Plain, Markdown, Html). Defaults to Markdown."), 4095 | timeToLive: z 4096 | .string() 4097 | .optional() 4098 | .describe( 4099 | "Time to live for ingested memory. Should be ISO 8601 format, for example, 'PT1H' for one hour, 'P1D' for one day, 'P7D' for one week, 'P30D' for one month. Doesn't support weeks or months explicitly." 4100 | ), 4101 | }, 4102 | async ({ name, text, textType, timeToLive }) => { 4103 | const client = new Graphlit(); 4104 | 4105 | try { 4106 | // TODO: need to add TTL parameter when available 4107 | const response = await client.ingestMemory(text, name, textType); 4108 | 4109 | return { 4110 | content: [ 4111 | { 4112 | type: "text", 4113 | text: JSON.stringify({ id: response.ingestMemory?.id }, null, 2), 4114 | }, 4115 | ], 4116 | }; 4117 | } catch (err: unknown) { 4118 | const error = err as Error; 4119 | return { 4120 | content: [ 4121 | { 4122 | type: "text", 4123 | text: `Error: ${error.message}`, 4124 | }, 4125 | ], 4126 | isError: true, 4127 | }; 4128 | } 4129 | } 4130 | ); 4131 | 4132 | server.tool( 4133 | "ingestFile", 4134 | `Ingests local file into Graphlit knowledge base. 4135 | Accepts the path to the file in the local filesystem. 4136 | Can use for storing *large* long-term textual memories or the output from LLM or other tools as content resources, which can be later searched or retrieved. 4137 | Executes asynchronously and returns the content identifier.`, 4138 | { 4139 | filePath: z 4140 | .string() 4141 | .describe("Path to the file in the local filesystem."), 4142 | }, 4143 | async ({ filePath }) => { 4144 | const client = new Graphlit(); 4145 | 4146 | try { 4147 | const fileName = path.basename(filePath); 4148 | const mimeType = mime.lookup(fileName) || "application/octet-stream"; 4149 | 4150 | const fileData = fs.readFileSync(filePath); 4151 | const base64Data = fileData.toString("base64"); 4152 | 4153 | const response = await client.ingestEncodedFile( 4154 | fileName, 4155 | base64Data, 4156 | mimeType 4157 | ); 4158 | 4159 | return { 4160 | content: [ 4161 | { 4162 | type: "text", 4163 | text: JSON.stringify( 4164 | { id: response.ingestEncodedFile?.id }, 4165 | null, 4166 | 2 4167 | ), 4168 | }, 4169 | ], 4170 | }; 4171 | } catch (err: unknown) { 4172 | const error = err as Error; 4173 | return { 4174 | content: [ 4175 | { 4176 | type: "text", 4177 | text: `Error: ${error.message}`, 4178 | }, 4179 | ], 4180 | isError: true, 4181 | }; 4182 | } 4183 | } 4184 | ); 4185 | 4186 | server.tool( 4187 | "screenshotPage", 4188 | `Screenshots web page from URL. 4189 | Executes *synchronously* and returns the content identifier.`, 4190 | { 4191 | url: z.string().describe("Web page URL."), 4192 | }, 4193 | async ({ url }) => { 4194 | const client = new Graphlit(); 4195 | 4196 | try { 4197 | const response = await client.screenshotPage(url, undefined, true); 4198 | 4199 | return { 4200 | content: [ 4201 | { 4202 | type: "text", 4203 | text: JSON.stringify( 4204 | { id: response.screenshotPage?.id }, 4205 | null, 4206 | 2 4207 | ), 4208 | }, 4209 | ], 4210 | }; 4211 | } catch (err: unknown) { 4212 | const error = err as Error; 4213 | return { 4214 | content: [ 4215 | { 4216 | type: "text", 4217 | text: `Error: ${error.message}`, 4218 | }, 4219 | ], 4220 | isError: true, 4221 | }; 4222 | } 4223 | } 4224 | ); 4225 | 4226 | server.tool( 4227 | "describeImageUrl", 4228 | `Prompts vision LLM and returns completion. 4229 | Does *not* ingest image into Graphlit knowledge base. 4230 | Accepts image URL as string. 4231 | Returns Markdown text from LLM completion.`, 4232 | { 4233 | prompt: z.string().describe("Prompt for image description."), 4234 | url: z.string().describe("Image URL."), 4235 | }, 4236 | async ({ prompt, url }) => { 4237 | const client = new Graphlit(); 4238 | 4239 | try { 4240 | const response = await client.describeImage(prompt, url); 4241 | 4242 | return { 4243 | content: [ 4244 | { 4245 | type: "text", 4246 | text: JSON.stringify( 4247 | { message: response.describeImage?.message }, 4248 | null, 4249 | 2 4250 | ), 4251 | }, 4252 | ], 4253 | }; 4254 | } catch (err: unknown) { 4255 | const error = err as Error; 4256 | return { 4257 | content: [ 4258 | { 4259 | type: "text", 4260 | text: `Error: ${error.message}`, 4261 | }, 4262 | ], 4263 | isError: true, 4264 | }; 4265 | } 4266 | } 4267 | ); 4268 | 4269 | server.tool( 4270 | "describeImageContent", 4271 | `Prompts vision LLM and returns description of image content. 4272 | Accepts content identifier as string, and optional prompt for image description. 4273 | Returns Markdown text from LLM completion.`, 4274 | { 4275 | id: z.string().describe("Content identifier."), 4276 | prompt: z 4277 | .string() 4278 | .optional() 4279 | .describe("Prompt for image description, optional."), 4280 | }, 4281 | async ({ prompt, id }) => { 4282 | const client = new Graphlit(); 4283 | 4284 | const DEFAULT_PROMPT = ` 4285 | Conduct a thorough analysis of the screenshot, with a particular emphasis on the textual content and any included imagery. 4286 | Provide a detailed examination of the text, highlighting key points and dissecting technical terms, named entities, and data presentations that contribute to the understanding of the subject matter. 4287 | Discuss how the technical language and the named entities relate to the overarching topic and objectives of the webpage. 4288 | Also, describe how the visual elements, such as color schemes, imagery, and branding elements like logos and taglines, support the textual message and enhance the viewer's comprehension of the content. 4289 | Assess the readability and organization of the content, and evaluate how these aspects facilitate the visitor's navigation and learning experience. Refrain from delving into the specifics of the user interface design but focus on the communication effectiveness and coherence of visual and textual elements. 4290 | Finally, offer a comprehensive view of the website's ability to convey its message and fulfill its intended commercial, educational, or promotional role, considering the target audience's perspective and potential engagement with the content. 4291 | 4292 | Carefully examine the image for any text it contains and extract as Markdown text. 4293 | In cases where the image contains no extractable text or only text that is not useful for understanding, don't extract any text. 4294 | Focus on including text that contributes significantly to understanding the image, such as titles, headings, key phrases, important data points, or labels. 4295 | Exclude any text that is not relevant or does not add value to the comprehension of the image. 4296 | Ensure to transcribe the text completely, without truncating with ellipses. 4297 | `; 4298 | 4299 | try { 4300 | const cresponse = await client.getContent(id); 4301 | const content = cresponse.content; 4302 | 4303 | if (content?.imageUri != null) { 4304 | const response = await client.describeImage( 4305 | prompt || DEFAULT_PROMPT, 4306 | content.imageUri 4307 | ); 4308 | 4309 | return { 4310 | content: [ 4311 | { 4312 | type: "text", 4313 | text: JSON.stringify( 4314 | { message: response.describeImage?.message }, 4315 | null, 4316 | 2 4317 | ), 4318 | }, 4319 | ], 4320 | }; 4321 | } else { 4322 | return { 4323 | content: [ 4324 | { 4325 | type: "text", 4326 | text: JSON.stringify({}, null, 2), 4327 | }, 4328 | ], 4329 | }; 4330 | } 4331 | } catch (err: unknown) { 4332 | const error = err as Error; 4333 | return { 4334 | content: [ 4335 | { 4336 | type: "text", 4337 | text: `Error: ${error.message}`, 4338 | }, 4339 | ], 4340 | isError: true, 4341 | }; 4342 | } 4343 | } 4344 | ); 4345 | 4346 | server.tool( 4347 | "publishAudio", 4348 | `Publishes text as audio format, and ingests into Graphlit knowledge base. 4349 | Accepts a name for the content object, the text itself, and an optional text type (Plain, Markdown, Html). Defaults to Markdown text type. 4350 | Optionally accepts an ElevenLabs voice identifier. 4351 | You *must* retrieve the content resource to get the downloadable audio URL for this published audio. 4352 | Executes *synchronously* and returns the content identifiers.`, 4353 | { 4354 | name: z.string().describe("Name for the content object."), 4355 | text: z.string().describe("Text content to publish."), 4356 | textType: z 4357 | .nativeEnum(TextTypes) 4358 | .optional() 4359 | .default(TextTypes.Markdown) 4360 | .describe("Text type (Plain, Markdown, Html). Defaults to Markdown."), 4361 | voice: z 4362 | .string() 4363 | .optional() 4364 | .default("HqW11As4VRPkApNPkAZp") 4365 | .describe("ElevenLabs voice identifier, optional."), 4366 | }, 4367 | async ({ name, text, textType, voice }) => { 4368 | const client = new Graphlit(); 4369 | 4370 | const type = ContentPublishingServiceTypes.ElevenLabsAudio; 4371 | const format = ContentPublishingFormats.Mp3; 4372 | const model = ElevenLabsModels.FlashV2_5; 4373 | 4374 | try { 4375 | const response = await client.publishText( 4376 | text, 4377 | textType, 4378 | { 4379 | type: type, 4380 | format: format, 4381 | elevenLabs: { model: model, voice: voice }, 4382 | }, 4383 | name, 4384 | undefined, 4385 | true 4386 | ); 4387 | 4388 | const contents = response.publishText?.contents 4389 | ?.map((content) => (content ? { id: content.id } : null)) 4390 | .filter(Boolean); 4391 | 4392 | return { 4393 | content: [ 4394 | { 4395 | type: "text", 4396 | text: JSON.stringify(contents, null, 2), 4397 | }, 4398 | ], 4399 | }; 4400 | } catch (err: unknown) { 4401 | const error = err as Error; 4402 | return { 4403 | content: [ 4404 | { 4405 | type: "text", 4406 | text: `Error: ${error.message}`, 4407 | }, 4408 | ], 4409 | isError: true, 4410 | }; 4411 | } 4412 | } 4413 | ); 4414 | 4415 | server.tool( 4416 | "publishImage", 4417 | `Publishes text as image format, and ingests into Graphlit knowledge base. 4418 | Accepts a name for the content object. 4419 | Also, accepts a prompt for image generation. For example, 'Create a cartoon image of a raccoon, saying "I Love Graphlit"'. 4420 | You *must* retrieve the content resource to get the downloadable image URL for this published image. 4421 | Executes *synchronously* and returns the content identifiers.`, 4422 | { 4423 | name: z.string().describe("Name for the content object."), 4424 | prompt: z.string().describe("Prompt for image generation."), 4425 | count: z 4426 | .number() 4427 | .optional() 4428 | .default(1) 4429 | .describe("Number of images to generate, optional. Defaults to 1."), 4430 | }, 4431 | async ({ name, prompt, count }) => { 4432 | const client = new Graphlit(); 4433 | 4434 | const type = ContentPublishingServiceTypes.OpenAiImage; 4435 | const format = ContentPublishingFormats.Png; 4436 | const model = OpenAiImageModels.GptImage_1; 4437 | 4438 | try { 4439 | const response = await client.publishText( 4440 | prompt, 4441 | TextTypes.Markdown, 4442 | { 4443 | type: type, 4444 | format: format, 4445 | openAIImage: { model: model, count: count }, 4446 | }, 4447 | name, 4448 | undefined, 4449 | true 4450 | ); 4451 | 4452 | const contents = response.publishText?.contents 4453 | ?.map((content) => (content ? { id: content.id } : null)) 4454 | .filter(Boolean); 4455 | 4456 | return { 4457 | content: [ 4458 | { 4459 | type: "text", 4460 | text: JSON.stringify(contents, null, 2), 4461 | }, 4462 | ], 4463 | }; 4464 | } catch (err: unknown) { 4465 | const error = err as Error; 4466 | return { 4467 | content: [ 4468 | { 4469 | type: "text", 4470 | text: `Error: ${error.message}`, 4471 | }, 4472 | ], 4473 | isError: true, 4474 | }; 4475 | } 4476 | } 4477 | ); 4478 | 4479 | server.tool( 4480 | "sendWebHookNotification", 4481 | `Sends a webhook notification to the provided URL. 4482 | Accepts the webhook URL. 4483 | Also accepts the text to be sent with the webhook, and an optional text type (Plain, Markdown, Html). Defaults to Markdown text type. 4484 | Returns true if the notification was successfully sent, or false otherwise.`, 4485 | { 4486 | url: z.string().describe("Webhook URL."), 4487 | text: z.string().describe("Text to send."), 4488 | textType: z 4489 | .nativeEnum(TextTypes) 4490 | .optional() 4491 | .default(TextTypes.Markdown) 4492 | .describe("Text type (Plain, Markdown, Html). Defaults to Markdown."), 4493 | }, 4494 | async ({ text, textType, url }) => { 4495 | const client = new Graphlit(); 4496 | 4497 | try { 4498 | const response = await client.sendNotification( 4499 | { type: IntegrationServiceTypes.WebHook, uri: url }, 4500 | text, 4501 | textType 4502 | ); 4503 | 4504 | return { 4505 | content: [ 4506 | { 4507 | type: "text", 4508 | text: JSON.stringify( 4509 | { success: response.sendNotification?.result }, 4510 | null, 4511 | 2 4512 | ), 4513 | }, 4514 | ], 4515 | }; 4516 | } catch (err: unknown) { 4517 | const error = err as Error; 4518 | return { 4519 | content: [ 4520 | { 4521 | type: "text", 4522 | text: `Error: ${error.message}`, 4523 | }, 4524 | ], 4525 | isError: true, 4526 | }; 4527 | } 4528 | } 4529 | ); 4530 | 4531 | server.tool( 4532 | "sendSlackNotification", 4533 | `Sends a Slack notification to the provided Slack channel. 4534 | Accepts the Slack channel name. 4535 | Also accepts the text for the Slack message, and an optional text type (Plain, Markdown, Html). Defaults to Markdown text type. 4536 | Hint: In Slack Markdown, images are displayed by simply putting the URL in angle brackets like instead of using the traditional Markdown image syntax ![alt text](url). 4537 | Requires environment variable to be configured: SLACK_BOT_TOKEN. 4538 | Returns true if the notification was successfully sent, or false otherwise.`, 4539 | { 4540 | channelName: z.string().describe("Slack channel name."), 4541 | text: z.string().describe("Text to send."), 4542 | textType: z 4543 | .nativeEnum(TextTypes) 4544 | .optional() 4545 | .default(TextTypes.Markdown) 4546 | .describe("Text type (Plain, Markdown, Html). Defaults to Markdown."), 4547 | }, 4548 | async ({ text, textType, channelName }) => { 4549 | const botToken = process.env.SLACK_BOT_TOKEN; 4550 | if (!botToken) { 4551 | console.error("Please set SLACK_BOT_TOKEN environment variable."); 4552 | process.exit(1); 4553 | } 4554 | 4555 | const client = new Graphlit(); 4556 | 4557 | try { 4558 | const response = await client.sendNotification( 4559 | { 4560 | type: IntegrationServiceTypes.Slack, 4561 | slack: { token: botToken, channel: channelName }, 4562 | }, 4563 | text, 4564 | textType 4565 | ); 4566 | 4567 | return { 4568 | content: [ 4569 | { 4570 | type: "text", 4571 | text: JSON.stringify( 4572 | { success: response.sendNotification?.result }, 4573 | null, 4574 | 2 4575 | ), 4576 | }, 4577 | ], 4578 | }; 4579 | } catch (err: unknown) { 4580 | const error = err as Error; 4581 | return { 4582 | content: [ 4583 | { 4584 | type: "text", 4585 | text: `Error: ${error.message}`, 4586 | }, 4587 | ], 4588 | isError: true, 4589 | }; 4590 | } 4591 | } 4592 | ); 4593 | 4594 | server.tool( 4595 | "sendTwitterNotification", 4596 | `Posts a tweet from the configured user account. 4597 | Accepts the plain text for the tweet. 4598 | Tweet text rules: allowed - plain text, @mentions, #hashtags, URLs (auto-shortened), line breaks (\n). 4599 | Not allowed - markdown, HTML tags, rich text, or custom styles. 4600 | Requires environment variables to be configured: TWITTER_CONSUMER_API_KEY, TWITTER_CONSUMER_API_SECRET, TWITTER_ACCESS_TOKEN_KEY, TWITTER_ACCESS_TOKEN_SECRET. 4601 | Returns true if the notification was successfully sent, or false otherwise.`, 4602 | { 4603 | text: z.string().describe("Text to send."), 4604 | }, 4605 | async ({ text }) => { 4606 | const consumerKey = process.env.TWITTER_CONSUMER_API_KEY; 4607 | if (!consumerKey) { 4608 | console.error( 4609 | "Please set TWITTER_CONSUMER_API_KEY environment variable." 4610 | ); 4611 | process.exit(1); 4612 | } 4613 | 4614 | const consumerSecret = process.env.TWITTER_CONSUMER_API_SECRET; 4615 | if (!consumerSecret) { 4616 | console.error( 4617 | "Please set TWITTER_CONSUMER_API_SECRET environment variable." 4618 | ); 4619 | process.exit(1); 4620 | } 4621 | 4622 | const accessTokenKey = process.env.TWITTER_ACCESS_TOKEN_KEY; 4623 | if (!accessTokenKey) { 4624 | console.error( 4625 | "Please set TWITTER_ACCESS_TOKEN_KEY environment variable." 4626 | ); 4627 | process.exit(1); 4628 | } 4629 | 4630 | const accessTokenSecret = process.env.TWITTER_ACCESS_TOKEN_SECRET; 4631 | if (!accessTokenSecret) { 4632 | console.error( 4633 | "Please set TWITTER_ACCESS_TOKEN_SECRET environment variable." 4634 | ); 4635 | process.exit(1); 4636 | } 4637 | 4638 | const client = new Graphlit(); 4639 | 4640 | try { 4641 | const response = await client.sendNotification( 4642 | { 4643 | type: IntegrationServiceTypes.Twitter, 4644 | twitter: { 4645 | consumerKey: consumerKey, 4646 | consumerSecret: consumerSecret, 4647 | accessTokenKey: accessTokenKey, 4648 | accessTokenSecret: accessTokenSecret, 4649 | }, 4650 | }, 4651 | text, 4652 | TextTypes.Plain 4653 | ); 4654 | 4655 | return { 4656 | content: [ 4657 | { 4658 | type: "text", 4659 | text: JSON.stringify( 4660 | { success: response.sendNotification?.result }, 4661 | null, 4662 | 2 4663 | ), 4664 | }, 4665 | ], 4666 | }; 4667 | } catch (err: unknown) { 4668 | const error = err as Error; 4669 | return { 4670 | content: [ 4671 | { 4672 | type: "text", 4673 | text: `Error: ${error.message}`, 4674 | }, 4675 | ], 4676 | isError: true, 4677 | }; 4678 | } 4679 | } 4680 | ); 4681 | 4682 | server.tool( 4683 | "sendEmailNotification", 4684 | `Sends an email notification to the provided email address(es). 4685 | Accepts the email subject and a list of email 'to' addresses. 4686 | Email addresses should be in RFC 5322 format. i.e. Alice Wonderland , or alice@wonderland.net 4687 | Also accepts the text for the email, and an optional text type (Plain, Markdown, Html). Defaults to Markdown text type. 4688 | Requires environment variable to be configured: FROM_EMAIL_ADDRESS. 4689 | Returns true if the notification was successfully sent, or false otherwise.`, 4690 | { 4691 | subject: z.string().describe("Email subject."), 4692 | to: z 4693 | .array(z.string()) 4694 | .describe("Email address(es) to send the notification to."), 4695 | text: z.string().describe("Text to send."), 4696 | textType: z 4697 | .nativeEnum(TextTypes) 4698 | .optional() 4699 | .default(TextTypes.Markdown) 4700 | .describe("Text type (Plain, Markdown, Html). Defaults to Markdown."), 4701 | }, 4702 | async ({ text, textType, subject, to }) => { 4703 | const from = process.env.FROM_EMAIL_ADDRESS; 4704 | if (!from) { 4705 | console.error("Please set FROM_EMAIL_ADDRESS environment variable."); 4706 | process.exit(1); 4707 | } 4708 | 4709 | const client = new Graphlit(); 4710 | 4711 | try { 4712 | const response = await client.sendNotification( 4713 | { type: IntegrationServiceTypes.Email, email: { subject, from, to } }, 4714 | text, 4715 | textType 4716 | ); 4717 | 4718 | return { 4719 | content: [ 4720 | { 4721 | type: "text", 4722 | text: JSON.stringify( 4723 | { success: response.sendNotification?.result }, 4724 | null, 4725 | 2 4726 | ), 4727 | }, 4728 | ], 4729 | }; 4730 | } catch (err: unknown) { 4731 | const error = err as Error; 4732 | return { 4733 | content: [ 4734 | { 4735 | type: "text", 4736 | text: `Error: ${error.message}`, 4737 | }, 4738 | ], 4739 | isError: true, 4740 | }; 4741 | } 4742 | } 4743 | ); 4744 | } 4745 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "declaration": true, 12 | "declarationMap": true 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules"] 16 | } 17 | --------------------------------------------------------------------------------