[
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\nweb_modules/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional stylelint cache\n.stylelintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variable files\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n.parcel-cache\n\n# Next.js build output\n.next\nout\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n.temp\n.cache\n\n# vitepress build output\n**/.vitepress/dist\n\n# vitepress cache directory\n**/.vitepress/cache\n\n# Docusaurus cache and generated files\n.docusaurus\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# yarn v2\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\nsrc/.DS_Store\n.DS_Store\nwebsite/static/img/Thumbs.db\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Merill Fernando\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Lokka\n\n[![npm version](https://badge.fury.io/js/@merill%2Flokka.svg)](https://badge.fury.io/js/@merill%2Flokka)\n\nLokka is a model-context-protocol server for the Microsoft Graph and Azure RM APIs that allows you to query and manage your Azure and Microsoft 365 tenants with AI.\n\n<img src=\"https://github.com/merill/lokka/blob/main/assets/lokka-demo-1.gif?raw=true\" alt=\"Lokka Demo - user create demo\" width=\"500\"/>\n\nPlease see [Lokka.dev](https://lokka.dev) for how to use Lokka with your favorite AI model and chat client.\n\nLokka lets you use Claude Desktop, or any MCP Client, to use natural language to accomplish things in your Azure and Microsoft 365 tenant through the Microsoft APIs.\n\ne.g.:\n\n- `Create a new security group called 'Sales and HR' with a dynamic rule based on the department attribute.` \n- `Find all the conditional access policies that haven't excluded the emergency access account`\n- `Show me all the Intune device configuration policies assigned to the 'Call center' group`\n- `What was the most expensive service in Azure last month?`\n\n![How does Lokka work?](https://github.com/merill/lokka/blob/main/website/docs/assets/how-does-lokka-mcp-server-work.png?raw=true)\n\n## Authentication Methods\n\nLokka now supports multiple authentication methods to accommodate different deployment scenarios:\n\n### Interactive Auth\n\nFor user-based authentication with interactive login, you can use the following configuration:\n\nThis is the simplest config and uses the default Lokka app.\n\n```json\n{\n  \"mcpServers\": {\n    \"Lokka-Microsoft\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@merill/lokka\"]\n    }\n  }\n}\n```\n\n#### Interactive auth with custom app\n\nIf you wish to use a custom Microsoft Entra app, you can create a new app registration in Microsoft Entra and configure it with the following environment variables:\n\n```json\n{\n  \"mcpServers\": {\n    \"Lokka-Microsoft\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@merill/lokka\"],\n      \"env\": {\n        \"TENANT_ID\": \"<tenant-id>\",\n        \"CLIENT_ID\": \"<client-id>\",\n        \"USE_INTERACTIVE\": \"true\"\n      }\n    }\n  }\n}\n```\n\n### App-Only Auth\n\nTraditional app-only authentication. You can use either certificate (recommended) or client secret authentication with the following configuration.\n\nSee [Install Guide](https://lokka.dev/docs/install) for more details on how to create an Entra app.\n\n#### App-Only Auth with Certificate\n\nApp only authentication using a PEM-encoded client certificate:\n\n```json\n{\n  \"mcpServers\": {\n    \"Lokka-Microsoft\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@merill/lokka\"],\n      \"env\": {\n        \"TENANT_ID\": \"<tenant-id>\",\n        \"CLIENT_ID\": \"<client-id>\",\n        \"CERTIFICATE_PATH\": \"/path/to/certificate.pem\",\n        \"CERTIFICATE_PASSWORD\": \"<optional-certificate-password>\",\n        \"USE_CERTIFICATE\": \"true\"\n      }\n    }\n  }\n}\n```\n\nFor comfort, in order to convert a PFX client certificate to a PEM-encoded certificate:\n\n```bash\nopenssl pkcs12 -in /path/to/cert.pfx -out /path/to/cert.pem -nodes -clcerts\n```\n\n#### App-Only Auth with Client Secret\n\n```json\n{\n  \"mcpServers\": {\n    \"Lokka-Microsoft\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@merill/lokka\"],\n      \"env\": {\n        \"TENANT_ID\": \"<tenant-id>\",\n        \"CLIENT_ID\": \"<client-id>\",\n        \"CLIENT_SECRET\": \"<client-secret>\"\n      }\n    }\n  }\n}\n```\n\n### Client-Provided Token\n\nToken-based authentication where the MCP Client provides access tokens:\n\n```json\n{\n  \"mcpServers\": {\n    \"Lokka-Microsoft\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@merill/lokka\"],\n      \"env\": {\n        \"USE_CLIENT_TOKEN\": \"true\"\n      }\n    }\n  }\n}\n```\n\nWhen using client-provided token mode:\n\n1. Start the MCP server with `USE_CLIENT_TOKEN=true`\n2. Use the `set-access-token` tool to provide a valid Microsoft Graph access token\n3. Use the `get-auth-status` tool to verify authentication status\n4. Refresh tokens as needed using `set-access-token`\n\n## New Tools\n\n### Token Management Tools\n\n- **`set-access-token`**: Set or update access tokens for Microsoft Graph authentication\n- **`get-auth-status`**: Check current authentication status and capabilities\n- **`add-graph-permission`**: Request additional Microsoft Graph permission scopes interactively\n\n### Graph API Version Control\n\nLokka now supports controlling the default Microsoft Graph API version used for all requests:\n\n- **Default behavior**: Uses `beta` version for access to latest features\n- **Production mode**: Set `USE_GRAPH_BETA=false` to force all requests to use `v1.0` version\n- **Per-request override**: You can still specify `graphApiVersion` parameter in individual requests (unless `USE_GRAPH_BETA=false`)\n\nWhen `USE_GRAPH_BETA=false`, all Graph API calls will use the stable `v1.0` version, even if `beta` is explicitly requested in the `graphApiVersion` parameter.\n\n## Getting started\n\nSee the docs for more information on how to install and configure Lokka.\n\n- [Introduction](https://lokka.dev/)\n- [Install guide](https://lokka.dev/docs/install)\n- [Developer guide](https://lokka.dev/docs/developer-guide)\n\n### One-click install for VS Code\n\n  | Platform | VS Code | VS Code Insiders |\n  | - | - | - |\n  | Windows | [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Lokka_for_Windows-0098FF?style=flat-square&logo=visualstudiocode&logoColor=ffffff)](vscode:mcp/install?%7B%22name%22%3A%22Lokka-Microsoft%22%2C%22type%22%3A%22stdio%22%2C%22command%22%3A%22cmd%22%2C%22args%22%3A%5B%22%2Fc%22%2C%22npx%22%2C%22-y%22%2C%22%40merill%2Flokka%22%5D%7D) | [![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Lokka_for_Windows-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=ffffff)](vscode-insiders:mcp/install?%7B%22name%22%3A%22Lokka-Microsoft%22%2C%22type%22%3A%22stdio%22%2C%22command%22%3A%22cmd%22%2C%22args%22%3A%5B%22%2Fc%22%2C%22npx%22%2C%22-y%22%2C%22%40merill%2Flokka%22%5D%7D) |\n  | macOS/Linux | [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Lokka_for_macOS_%26_Linux-0098FF?style=flat-square&logo=visualstudiocode&logoColor=ffffff)](vscode:mcp/install?%7B%22name%22%3A%22Lokka-Microsoft%22%2C%22type%22%3A%22stdio%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40merill%2Flokka%22%5D%7D) | [![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Lokka_for_macOS_%26_Linux-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=ffffff)](vscode-insiders:mcp/install?%7B%22name%22%3A%22Lokka-Microsoft%22%2C%22type%22%3A%22stdio%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40merill%2Flokka%22%5D%7D) |\n\n\n## Components\n\n### Tools\n\n1. `Lokka-Microsoft`\n   - Call Microsoft Graph & Azure APIs. Supports querying Azure and Microsoft 365 tenants. Updates are also supported if permissions are provided.\n   - Input:\n     - `apiType` (string): Type of Microsoft API to query. Options: 'graph' for Microsoft Graph (Entra) or 'azure' for Azure Resource Management.\n     - `path` (string): The Azure or Graph API URL path to call (e.g. '/users', '/groups', '/subscriptions').\n     - `method` (string): HTTP method to use (e.g., get, post, put, patch, delete)\n     - `apiVersion` (string): Azure Resource Management API version (required for apiType Azure)\n     - `subscriptionId` (string): Azure Subscription ID (for Azure Resource Management).\n     - `queryParams` (string): Array of query parameters like $filter, $select, etc. All parameters are strings.\n     - `body` (JSON): The request body (for POST, PUT, PATCH)\n   - Returns: Results from the Azure or Graph API call.\n\n2. `set-access-token` *(New in v0.2.0)*\n   - Set or update an access token for Microsoft Graph authentication when using client-provided token mode.\n   - Input:\n     - `accessToken` (string): The access token obtained from Microsoft Graph authentication\n     - `expiresOn` (string, optional): Token expiration time in ISO format\n   - Returns: Confirmation of token update\n\n3. `get-auth-status` *(New in v0.2.0)*\n   - Check the current authentication status and mode of the MCP Server\n   - Returns: Authentication mode, readiness status, and capabilities\n\n### Environment Variables\n\nThe configuration of the server is done using environment variables. The following environment variables are supported:\n\n| Name | Description | Required |\n|------|-------------|----------|\n| `TENANT_ID` | The ID of the Microsoft Entra tenant. | Yes (except for client-provided token mode) |\n| `CLIENT_ID` | The ID of the application registered in Microsoft Entra. | Yes (except for client-provided token mode) |\n| `CLIENT_SECRET` | The client secret of the application registered in Microsoft Entra. | Yes (for client credentials mode only) |\n| `USE_INTERACTIVE` | Set to \"true\" to enable interactive authentication mode. | No |\n| `USE_CLIENT_TOKEN` | Set to \"true\" to enable client-provided token authentication mode. | No |\n| `USE_CERTIFICATE` | Set to \"true\" to enable certificate authentication mode. | No |\n| `CERTIFICATE_PATH` | Path to the PEM-encoded certificate file for certificate authentication. | Yes (for certificate mode only) |\n| `CERTIFICATE_PASSWORD` | Password for the certificate file (if encrypted). | No |\n| `REDIRECT_URI` | Redirect URI for interactive authentication (default: `http://localhost:3000`). | No |\n| `ACCESS_TOKEN` | Initial access token for client-provided token mode. | No |\n| `USE_GRAPH_BETA` | Set to \"false\" to force all Graph API calls to use v1.0 instead of beta (default: true, allows beta). | No |\n\n## Contributors\n\n- Interactive and Token-based Authentication (v0.2.0) - [@darrenjrobinson](https://github.com/darrenjrobinson)\n- Certificate Authentication (v0.2.1) - [@nitzpo](https://github.com/nitzpo)\n\n## Installation\n\nTo use this server with the Claude Desktop app, add the following configuration to the \"mcpServers\" section of your\n`claude_desktop_config.json`:\n\n### Interactive Authentication\n\n```json\n{\n  \"mcpServers\": {\n    \"Lokka-Microsoft\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@merill/lokka\"]\n    }\n  }\n}\n```\n\n### Client Credentials Authentication\n\n```json\n{\n  \"mcpServers\": {\n    \"Lokka-Microsoft\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@merill/lokka\"],\n      \"env\": {\n        \"TENANT_ID\": \"<tenant-id>\",\n        \"CLIENT_ID\": \"<client-id>\",\n        \"CLIENT_SECRET\": \"<client-secret>\"\n      }\n    }\n  }\n}\n```\n\nMake sure to replace `<tenant-id>`, `<client-id>`, and `<client-secret>` with the actual values from your Microsoft Entra application. (See [Install Guide](https://lokka.dev/docs/install) for more details on how to create an Entra app and configure the agent.)\n"
  },
  {
    "path": "src/mcp/README.md",
    "content": "# Lokka\n\n[![npm version](https://badge.fury.io/js/@merill%2Flokka.svg)](https://badge.fury.io/js/@merill%2Flokka)\n\nLokka is a model-context-protocol server for the Microsoft Graph and Azure RM APIs that allows you to query and manage your Azure and Microsoft 365 tenants with AI.\n\n<img src=\"https://github.com/merill/lokka/blob/main/assets/lokka-demo-1.gif?raw=true\" alt=\"Lokka Demo - user create demo\" width=\"500\"/>\n\nPlease see [Lokka.dev](https://lokka.dev) for how to use Lokka with your favorite AI model and chat client.\n\nLokka lets you use Claude Desktop, or any MCP Client, to use natural language to accomplish things in your Azure and Microsoft 365 tenant through the Microsoft APIs.\n\ne.g.:\n\n- `Create a new security group called 'Sales and HR' with a dynamic rule based on the department attribute.` \n- `Find all the conditional access policies that haven't excluded the emergency access account`\n- `Show me all the Intune device configuration policies assigned to the 'Call center' group`\n- `What was the most expensive service in Azure last month?`\n\n![How does Lokka work?](https://github.com/merill/lokka/blob/main/website/docs/assets/how-does-lokka-mcp-server-work.png?raw=true)\n\n## Authentication Methods\n\nLokka now supports multiple authentication methods to accommodate different deployment scenarios:\n\n### Interactive Auth\n\nFor user-based authentication with interactive login, you can use the following configuration:\n\nThis is the simplest config and uses the default Lokka app.\n\n```json\n{\n  \"mcpServers\": {\n    \"Lokka-Microsoft\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@merill/lokka\"]\n    }\n  }\n}\n```\n\n#### Interactive auth with custom app\n\nIf you wish to use a custom Microsoft Entra app, you can create a new app registration in Microsoft Entra and configure it with the following environment variables:\n\n```json\n{\n  \"mcpServers\": {\n    \"Lokka-Microsoft\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@merill/lokka\"],\n      \"env\": {\n        \"TENANT_ID\": \"<tenant-id>\",\n        \"CLIENT_ID\": \"<client-id>\",\n        \"USE_INTERACTIVE\": \"true\"\n      }\n    }\n  }\n}\n```\n\n### App-Only Auth\n\nTraditional app-only authentication. You can use either certificate (recommended) or client secret authentication with the following configuration.\n\nSee [Install Guide](https://lokka.dev/docs/install) for more details on how to create an Entra app.\n\n#### App-Only Auth with Certificate\n\nApp only authentication using a PEM-encoded client certificate:\n\n```json\n{\n  \"mcpServers\": {\n    \"Lokka-Microsoft\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@merill/lokka\"],\n      \"env\": {\n        \"TENANT_ID\": \"<tenant-id>\",\n        \"CLIENT_ID\": \"<client-id>\",\n        \"CERTIFICATE_PATH\": \"/path/to/certificate.pem\",\n        \"CERTIFICATE_PASSWORD\": \"<optional-certificate-password>\",\n        \"USE_CERTIFICATE\": \"true\"\n      }\n    }\n  }\n}\n```\n\nFor comfort, in order to convert a PFX client certificate to a PEM-encoded certificate:\n\n```bash\nopenssl pkcs12 -in /path/to/cert.pfx -out /path/to/cert.pem -nodes -clcerts\n```\n\n#### #### App-Only Auth with Client Secret\n\n```json\n{\n  \"mcpServers\": {\n    \"Lokka-Microsoft\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@merill/lokka\"],\n      \"env\": {\n        \"TENANT_ID\": \"<tenant-id>\",\n        \"CLIENT_ID\": \"<client-id>\",\n        \"CLIENT_SECRET\": \"<client-secret>\"\n      }\n    }\n  }\n}\n```\n\n### Client-Provided Token\n\nToken-based authentication where the MCP Client provides access tokens:\n\n```json\n{\n  \"mcpServers\": {\n    \"Lokka-Microsoft\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@merill/lokka\"],\n      \"env\": {\n        \"USE_CLIENT_TOKEN\": \"true\"\n      }\n    }\n  }\n}\n```\n\nWhen using client-provided token mode:\n\n1. Start the MCP server with `USE_CLIENT_TOKEN=true`\n2. Use the `set-access-token` tool to provide a valid Microsoft Graph access token\n3. Use the `get-auth-status` tool to verify authentication status\n4. Refresh tokens as needed using `set-access-token`\n\n## New Tools\n\n### Token Management Tools\n\n- **`set-access-token`**: Set or update access tokens for Microsoft Graph authentication\n- **`get-auth-status`**: Check current authentication status and capabilities\n- **`add-graph-permission`**: Request additional Microsoft Graph permission scopes interactively\n\n### Graph API Version Control\n\nLokka now supports controlling the default Microsoft Graph API version used for all requests:\n\n- **Default behavior**: Uses `beta` version for access to latest features\n- **Production mode**: Set `USE_GRAPH_BETA=false` to force all requests to use `v1.0` version\n- **Per-request override**: You can still specify `graphApiVersion` parameter in individual requests (unless `USE_GRAPH_BETA=false`)\n\nWhen `USE_GRAPH_BETA=false`, all Graph API calls will use the stable `v1.0` version, even if `beta` is explicitly requested in the `graphApiVersion` parameter.\n\n## Getting started\n\nSee the docs for more information on how to install and configure Lokka.\n\n- [Introduction](https://lokka.dev/)\n- [Install guide](https://lokka.dev/docs/install)\n- [Developer guide](https://lokka.dev/docs/developer-guide)\n\n## Components\n\n### Tools\n\n1. `Lokka-Microsoft`\n   - Call Microsoft Graph & Azure APIs. Supports querying Azure and Microsoft 365 tenants. Updates are also supported if permissions are provided.\n   - Input:\n     - `apiType` (string): Type of Microsoft API to query. Options: 'graph' for Microsoft Graph (Entra) or 'azure' for Azure Resource Management.\n     - `path` (string): The Azure or Graph API URL path to call (e.g. '/users', '/groups', '/subscriptions').\n     - `method` (string): HTTP method to use (e.g., get, post, put, patch, delete)\n     - `apiVersion` (string): Azure Resource Management API version (required for apiType Azure)\n     - `subscriptionId` (string): Azure Subscription ID (for Azure Resource Management).\n     - `queryParams` (string): Array of query parameters like $filter, $select, etc. All parameters are strings.\n     - `body` (JSON): The request body (for POST, PUT, PATCH)\n   - Returns: Results from the Azure or Graph API call.\n\n2. `set-access-token` *(New in v0.2.0)*\n   - Set or update an access token for Microsoft Graph authentication when using client-provided token mode.\n   - Input:\n     - `accessToken` (string): The access token obtained from Microsoft Graph authentication\n     - `expiresOn` (string, optional): Token expiration time in ISO format\n   - Returns: Confirmation of token update\n\n3. `get-auth-status` *(New in v0.2.0)*\n   - Check the current authentication status and mode of the MCP Server\n   - Returns: Authentication mode, readiness status, and capabilities\n\n### Environment Variables\n\nThe configuration of the server is done using environment variables. The following environment variables are supported:\n\n| Name | Description | Required |\n|------|-------------|----------|\n| `TENANT_ID` | The ID of the Microsoft Entra tenant. | Yes (except for client-provided token mode) |\n| `CLIENT_ID` | The ID of the application registered in Microsoft Entra. | Yes (except for client-provided token mode) |\n| `CLIENT_SECRET` | The client secret of the application registered in Microsoft Entra. | Yes (for client credentials mode only) |\n| `USE_INTERACTIVE` | Set to \"true\" to enable interactive authentication mode. | No |\n| `USE_CLIENT_TOKEN` | Set to \"true\" to enable client-provided token authentication mode. | No |\n| `USE_CERTIFICATE` | Set to \"true\" to enable certificate authentication mode. | No |\n| `CERTIFICATE_PATH` | Path to the PEM-encoded certificate file for certificate authentication. | Yes (for certificate mode only) |\n| `CERTIFICATE_PASSWORD` | Password for the certificate file (if encrypted). | No |\n| `REDIRECT_URI` | Redirect URI for interactive authentication (default: `http://localhost:3200`). | No |\n| `ACCESS_TOKEN` | Initial access token for client-provided token mode. | No |\n| `USE_GRAPH_BETA` | Set to \"false\" to force all Graph API calls to use v1.0 instead of beta (default: true, allows beta). | No |\n\n## Contributors\n\n- Interactive and Token-based Authentication (v0.2.0) - [@darrenjrobinson](https://github.com/darrenjrobinson)\n- Certificate Authentication (v0.2.1) - [@nitzpo](https://github.com/nitzpo)\n\n## Installation\n\nTo use this server with the Claude Desktop app, add the following configuration to the \"mcpServers\" section of your\n`claude_desktop_config.json`:\n\n### Interactive Authentication\n\n```json\n{\n  \"mcpServers\": {\n    \"Lokka-Microsoft\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@merill/lokka\"]\n    }\n  }\n}\n```\n\n### Client Credentials Authentication\n\n```json\n{\n  \"mcpServers\": {\n    \"Lokka-Microsoft\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@merill/lokka\"],\n      \"env\": {\n        \"TENANT_ID\": \"<tenant-id>\",\n        \"CLIENT_ID\": \"<client-id>\",\n        \"CLIENT_SECRET\": \"<client-secret>\"\n      }\n    }\n  }\n}\n```\n\nMake sure to replace `<tenant-id>`, `<client-id>`, and `<client-secret>` with the actual values from your Microsoft Entra application. (See [Install Guide](https://lokka.dev/docs/install) for more details on how to create an Entra app and configure the agent.)\n"
  },
  {
    "path": "src/mcp/TESTING.md",
    "content": "# How to Start Lokka MCP Server Locally and Test Microsoft Graph\n\nThis guide shows you how to start the Lokka MCP Server locally and test it with real Microsoft Graph API requests.\n\n## Prerequisites\n\n1. **Node.js** installed (v16 or later)\n2. **Valid Microsoft Graph access token** (see below for how to get one)\n3. **Build the project**: `npm run build`\n\n## Getting an Access Token\n\n### Option 1: Azure CLI (Easiest)\n\n```bash\n# Login to Azure CLI\naz login\n\n# Get a token for Microsoft Graph\naz account get-access-token --resource https://graph.microsowft.com --query accessToken -o tsv\n```\n\n### Option 2: Graph Explorer (Quick Testing)\n\n1. Go to https://developer.microsoft.com/en-us/graph/graph-explorer\n2. Sign in with your Microsoft account\n3. Open browser developer tools (F12)\n4. Go to Network tab\n5. Make any Graph request (like GET /me)\n6. Find the request in Network tab\n7. Copy the Authorization header value (remove \"Bearer \" prefix)\n"
  },
  {
    "path": "src/mcp/build/auth.js",
    "content": "import { ClientSecretCredential, ClientCertificateCredential, InteractiveBrowserCredential, DeviceCodeCredential } from \"@azure/identity\";\nimport jwt from \"jsonwebtoken\";\nimport { logger } from \"./logger.js\";\nimport { LokkaClientId, LokkaDefaultTenantId, LokkaDefaultRedirectUri } from \"./constants.js\";\n// Constants\nconst ONE_HOUR_IN_MS = 60 * 60 * 1000; // One hour in milliseconds\n// Helper function to parse JWT and extract scopes\nfunction parseJwtScopes(token) {\n    try {\n        // Decode JWT without verifying signature (we trust the token from Azure Identity)\n        const decoded = jwt.decode(token);\n        if (!decoded || typeof decoded !== 'object') {\n            logger.info(\"Failed to decode JWT token\");\n            return [];\n        }\n        // Extract scopes from the 'scp' claim (space-separated string)\n        const scopesString = decoded.scp;\n        if (typeof scopesString === 'string') {\n            return scopesString.split(' ').filter(scope => scope.length > 0);\n        }\n        // Some tokens might have roles instead of scopes\n        const roles = decoded.roles;\n        if (Array.isArray(roles)) {\n            return roles;\n        }\n        logger.info(\"No scopes found in JWT token\");\n        return [];\n    }\n    catch (error) {\n        logger.error(\"Error parsing JWT token for scopes\", error);\n        return [];\n    }\n}\n// Simple authentication provider that works with Azure Identity TokenCredential\nexport class TokenCredentialAuthProvider {\n    credential;\n    constructor(credential) {\n        this.credential = credential;\n    }\n    async getAccessToken() {\n        const token = await this.credential.getToken(\"https://graph.microsoft.com/.default\");\n        if (!token) {\n            throw new Error(\"Failed to acquire access token\");\n        }\n        return token.token;\n    }\n}\nexport class ClientProvidedTokenCredential {\n    accessToken;\n    expiresOn;\n    constructor(accessToken, expiresOn) {\n        if (accessToken) {\n            this.accessToken = accessToken;\n            this.expiresOn = expiresOn || new Date(Date.now() + ONE_HOUR_IN_MS); // Default 1 hour\n        }\n        else {\n            this.expiresOn = new Date(0); // Set to epoch to indicate no valid token\n        }\n    }\n    async getToken(scopes) {\n        if (!this.accessToken || !this.expiresOn || this.expiresOn <= new Date()) {\n            logger.error(\"Access token is not available or has expired\");\n            return null;\n        }\n        return {\n            token: this.accessToken,\n            expiresOnTimestamp: this.expiresOn.getTime()\n        };\n    }\n    updateToken(accessToken, expiresOn) {\n        this.accessToken = accessToken;\n        this.expiresOn = expiresOn || new Date(Date.now() + ONE_HOUR_IN_MS);\n        logger.info(\"Access token updated successfully\");\n    }\n    isExpired() {\n        return !this.expiresOn || this.expiresOn <= new Date();\n    }\n    getExpirationTime() {\n        return this.expiresOn || new Date(0);\n    }\n    // Getter for access token (for internal use by AuthManager)\n    getAccessToken() {\n        return this.accessToken;\n    }\n}\nexport var AuthMode;\n(function (AuthMode) {\n    AuthMode[\"ClientCredentials\"] = \"client_credentials\";\n    AuthMode[\"ClientProvidedToken\"] = \"client_provided_token\";\n    AuthMode[\"Interactive\"] = \"interactive\";\n    AuthMode[\"Certificate\"] = \"certificate\";\n})(AuthMode || (AuthMode = {}));\nexport class AuthManager {\n    credential = null;\n    config;\n    constructor(config) {\n        this.config = config;\n    }\n    async initialize() {\n        switch (this.config.mode) {\n            case AuthMode.ClientCredentials:\n                if (!this.config.tenantId || !this.config.clientId || !this.config.clientSecret) {\n                    throw new Error(\"Client credentials mode requires tenantId, clientId, and clientSecret\");\n                }\n                logger.info(\"Initializing Client Credentials authentication\");\n                this.credential = new ClientSecretCredential(this.config.tenantId, this.config.clientId, this.config.clientSecret);\n                break;\n            case AuthMode.ClientProvidedToken:\n                logger.info(\"Initializing Client Provided Token authentication\");\n                this.credential = new ClientProvidedTokenCredential(this.config.accessToken, this.config.expiresOn);\n                break;\n            case AuthMode.Certificate:\n                if (!this.config.tenantId || !this.config.clientId || !this.config.certificatePath) {\n                    throw new Error(\"Certificate mode requires tenantId, clientId, and certificatePath\");\n                }\n                logger.info(\"Initializing Certificate authentication\");\n                this.credential = new ClientCertificateCredential(this.config.tenantId, this.config.clientId, {\n                    certificatePath: this.config.certificatePath,\n                    certificatePassword: this.config.certificatePassword\n                });\n                break;\n            case AuthMode.Interactive:\n                // Use defaults if not provided\n                const tenantId = this.config.tenantId || LokkaDefaultTenantId;\n                const clientId = this.config.clientId || LokkaClientId;\n                logger.info(`Initializing Interactive authentication with tenant ID: ${tenantId}, client ID: ${clientId}`);\n                try {\n                    // Try Interactive Browser first\n                    this.credential = new InteractiveBrowserCredential({\n                        tenantId: tenantId,\n                        clientId: clientId,\n                        redirectUri: this.config.redirectUri || LokkaDefaultRedirectUri,\n                    });\n                }\n                catch (error) {\n                    // Fallback to Device Code flow\n                    logger.info(\"Interactive browser failed, falling back to device code flow\");\n                    this.credential = new DeviceCodeCredential({\n                        tenantId: tenantId,\n                        clientId: clientId,\n                        userPromptCallback: (info) => {\n                            console.log(`\\n🔐 Authentication Required:`);\n                            console.log(`Please visit: ${info.verificationUri}`);\n                            console.log(`And enter code: ${info.userCode}\\n`);\n                            return Promise.resolve();\n                        },\n                    });\n                }\n                break;\n            default:\n                throw new Error(`Unsupported authentication mode: ${this.config.mode}`);\n        }\n        // Test the credential\n        await this.testCredential();\n    }\n    updateAccessToken(accessToken, expiresOn) {\n        if (this.config.mode === AuthMode.ClientProvidedToken && this.credential instanceof ClientProvidedTokenCredential) {\n            this.credential.updateToken(accessToken, expiresOn);\n        }\n        else {\n            throw new Error(\"Token update only supported in client provided token mode\");\n        }\n    }\n    async testCredential() {\n        if (!this.credential) {\n            throw new Error(\"Credential not initialized\");\n        }\n        // Skip testing if ClientProvidedToken mode has no initial token\n        if (this.config.mode === AuthMode.ClientProvidedToken && !this.config.accessToken) {\n            logger.info(\"Skipping initial credential test as no token was provided at startup.\");\n            return;\n        }\n        try {\n            const token = await this.credential.getToken(\"https://graph.microsoft.com/.default\");\n            if (!token) {\n                throw new Error(\"Failed to acquire token\");\n            }\n            logger.info(\"Authentication successful\");\n        }\n        catch (error) {\n            logger.error(\"Authentication test failed\", error);\n            throw error;\n        }\n    }\n    getGraphAuthProvider() {\n        if (!this.credential) {\n            throw new Error(\"Authentication not initialized\");\n        }\n        return new TokenCredentialAuthProvider(this.credential);\n    }\n    getAzureCredential() {\n        if (!this.credential) {\n            throw new Error(\"Authentication not initialized\");\n        }\n        return this.credential;\n    }\n    getAuthMode() {\n        return this.config.mode;\n    }\n    isClientCredentials() {\n        return this.config.mode === AuthMode.ClientCredentials;\n    }\n    isClientProvidedToken() {\n        return this.config.mode === AuthMode.ClientProvidedToken;\n    }\n    isInteractive() {\n        return this.config.mode === AuthMode.Interactive;\n    }\n    async getTokenStatus() {\n        if (this.credential instanceof ClientProvidedTokenCredential) {\n            const tokenStatus = {\n                isExpired: this.credential.isExpired(),\n                expiresOn: this.credential.getExpirationTime()\n            };\n            // If we have a valid token, parse it to extract scopes\n            if (!tokenStatus.isExpired) {\n                const accessToken = this.credential.getAccessToken();\n                if (accessToken) {\n                    try {\n                        const scopes = parseJwtScopes(accessToken);\n                        return {\n                            ...tokenStatus,\n                            scopes: scopes\n                        };\n                    }\n                    catch (error) {\n                        logger.error(\"Error parsing token scopes in getTokenStatus\", error);\n                        return tokenStatus;\n                    }\n                }\n            }\n            return tokenStatus;\n        }\n        else if (this.credential) {\n            // For other credential types, try to get a fresh token and parse it\n            try {\n                const accessToken = await this.credential.getToken(\"https://graph.microsoft.com/.default\");\n                if (accessToken && accessToken.token) {\n                    const scopes = parseJwtScopes(accessToken.token);\n                    return {\n                        isExpired: false,\n                        expiresOn: new Date(accessToken.expiresOnTimestamp),\n                        scopes: scopes\n                    };\n                }\n            }\n            catch (error) {\n                logger.error(\"Error getting token for scope parsing\", error);\n            }\n        }\n        return { isExpired: false };\n    }\n}\n"
  },
  {
    "path": "src/mcp/build/constants.js",
    "content": "// Shared constants for the Lokka MCP Server\nexport const LokkaClientId = \"a9bac4c3-af0d-4292-9453-9da89e390140\";\nexport const LokkaDefaultTenantId = \"common\";\nexport const LokkaDefaultRedirectUri = \"http://localhost:3000\";\n// Default Graph API version based on USE_GRAPH_BETA environment variable\nexport const getDefaultGraphApiVersion = () => {\n    return process.env.USE_GRAPH_BETA !== 'false' ? \"beta\" : \"v1.0\";\n};\n"
  },
  {
    "path": "src/mcp/build/logger.js",
    "content": "import { appendFileSync } from \"fs\";\nimport { join } from \"path\";\nconst LOG_FILE = join(import.meta.dirname, \"mcp-server.log\");\nfunction formatMessage(level, message, data) {\n    const timestamp = new Date().toISOString();\n    const dataStr = data\n        ? `\\n${JSON.stringify(data, null, 2)}`\n        : \"\";\n    return `[${timestamp}] [${level}] ${message}${dataStr}\\n`;\n}\nexport const logger = {\n    info(message, data) {\n        const logMessage = formatMessage(\"INFO\", message, data);\n        appendFileSync(LOG_FILE, logMessage);\n    },\n    error(message, error) {\n        const logMessage = formatMessage(\"ERROR\", message, error);\n        appendFileSync(LOG_FILE, logMessage);\n    },\n    // debug(message: string, data?: unknown) {\n    //   const logMessage = formatMessage(\n    //     \"DEBUG\",\n    //     message,\n    //     data,\n    //   );\n    //   appendFileSync(LOG_FILE, logMessage);\n    // },\n};\n"
  },
  {
    "path": "src/mcp/build/main.js",
    "content": "#!/usr/bin/env node\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\nimport { Client, PageIterator } from \"@microsoft/microsoft-graph-client\";\nimport fetch from 'isomorphic-fetch'; // Required polyfill for Graph client\nimport { logger } from \"./logger.js\";\nimport { AuthManager, AuthMode } from \"./auth.js\";\nimport { LokkaClientId, LokkaDefaultTenantId, LokkaDefaultRedirectUri, getDefaultGraphApiVersion } from \"./constants.js\";\n// Set up global fetch for the Microsoft Graph client\nglobal.fetch = fetch;\n// Create server instance\nconst server = new McpServer({\n    name: \"Lokka-Microsoft\",\n    version: \"0.2.0\", // Updated version for token-based auth support\n});\nlogger.info(\"Starting Lokka Multi-Microsoft API MCP Server (v0.2.0 - Token-Based Auth Support)\");\n// Initialize authentication and clients\nlet authManager = null;\nlet graphClient = null;\n// Check USE_GRAPH_BETA environment variable\nconst useGraphBeta = process.env.USE_GRAPH_BETA !== 'false'; // Default to true unless explicitly set to 'false'\nconst defaultGraphApiVersion = getDefaultGraphApiVersion();\nlogger.info(`Graph API default version: ${defaultGraphApiVersion} (USE_GRAPH_BETA=${process.env.USE_GRAPH_BETA || 'undefined'})`);\nserver.tool(\"Lokka-Microsoft\", \"A versatile tool to interact with Microsoft APIs including Microsoft Graph (Entra) and Azure Resource Management. IMPORTANT: For Graph API GET requests using advanced query parameters ($filter, $count, $search, $orderby), you are ADVISED to set 'consistencyLevel: \\\"eventual\\\"'.\", {\n    apiType: z.enum([\"graph\", \"azure\"]).describe(\"Type of Microsoft API to query. Options: 'graph' for Microsoft Graph (Entra) or 'azure' for Azure Resource Management.\"),\n    path: z.string().describe(\"The Azure or Graph API URL path to call (e.g. '/users', '/groups', '/subscriptions')\"),\n    method: z.enum([\"get\", \"post\", \"put\", \"patch\", \"delete\"]).describe(\"HTTP method to use\"),\n    apiVersion: z.string().optional().describe(\"Azure Resource Management API version (required for apiType Azure)\"),\n    subscriptionId: z.string().optional().describe(\"Azure Subscription ID (for Azure Resource Management).\"),\n    queryParams: z.record(z.string()).optional().describe(\"Query parameters for the request\"),\n    body: z.record(z.string(), z.any()).optional().describe(\"The request body (for POST, PUT, PATCH)\"),\n    graphApiVersion: z.enum([\"v1.0\", \"beta\"]).optional().default(defaultGraphApiVersion).describe(`Microsoft Graph API version to use (default: ${defaultGraphApiVersion})`),\n    fetchAll: z.boolean().optional().default(false).describe(\"Set to true to automatically fetch all pages for list results (e.g., users, groups). Default is false.\"),\n    consistencyLevel: z.string().optional().describe(\"Graph API ConsistencyLevel header. ADVISED to be set to 'eventual' for Graph GET requests using advanced query parameters ($filter, $count, $search, $orderby).\"),\n}, async ({ apiType, path, method, apiVersion, subscriptionId, queryParams, body, graphApiVersion, fetchAll, consistencyLevel }) => {\n    // Override graphApiVersion if USE_GRAPH_BETA is explicitly set to false\n    const effectiveGraphApiVersion = !useGraphBeta ? \"v1.0\" : graphApiVersion;\n    logger.info(`Executing Lokka-Microsoft tool with params: apiType=${apiType}, path=${path}, method=${method}, graphApiVersion=${effectiveGraphApiVersion}, fetchAll=${fetchAll}, consistencyLevel=${consistencyLevel}`);\n    let determinedUrl;\n    try {\n        let responseData;\n        // --- Microsoft Graph Logic ---\n        if (apiType === 'graph') {\n            if (!graphClient) {\n                throw new Error(\"Graph client not initialized\");\n            }\n            determinedUrl = `https://graph.microsoft.com/${effectiveGraphApiVersion}`; // For error reporting\n            // Construct the request using the Graph SDK client\n            let request = graphClient.api(path).version(effectiveGraphApiVersion);\n            // Add query parameters if provided and not empty\n            if (queryParams && Object.keys(queryParams).length > 0) {\n                request = request.query(queryParams);\n            }\n            // Add ConsistencyLevel header if provided\n            if (consistencyLevel) {\n                request = request.header('ConsistencyLevel', consistencyLevel);\n                logger.info(`Added ConsistencyLevel header: ${consistencyLevel}`);\n            }\n            // Handle different methods\n            switch (method.toLowerCase()) {\n                case 'get':\n                    if (fetchAll) {\n                        logger.info(`Fetching all pages for Graph path: ${path}`);\n                        // Fetch the first page to get context and initial data\n                        const firstPageResponse = await request.get();\n                        const odataContext = firstPageResponse['@odata.context']; // Capture context from first page\n                        let allItems = firstPageResponse.value || []; // Initialize with first page's items\n                        // Callback function to process subsequent pages\n                        const callback = (item) => {\n                            allItems.push(item);\n                            return true; // Return true to continue iteration\n                        };\n                        // Create a PageIterator starting from the first response\n                        const pageIterator = new PageIterator(graphClient, firstPageResponse, callback);\n                        // Iterate over all remaining pages\n                        await pageIterator.iterate();\n                        // Construct final response with context and combined values under 'value' key\n                        responseData = {\n                            '@odata.context': odataContext,\n                            value: allItems\n                        };\n                        logger.info(`Finished fetching all Graph pages. Total items: ${allItems.length}`);\n                    }\n                    else {\n                        logger.info(`Fetching single page for Graph path: ${path}`);\n                        responseData = await request.get();\n                    }\n                    break;\n                case 'post':\n                    responseData = await request.post(body ?? {});\n                    break;\n                case 'put':\n                    responseData = await request.put(body ?? {});\n                    break;\n                case 'patch':\n                    responseData = await request.patch(body ?? {});\n                    break;\n                case 'delete':\n                    responseData = await request.delete(); // Delete often returns no body or 204\n                    // Handle potential 204 No Content response\n                    if (responseData === undefined || responseData === null) {\n                        responseData = { status: \"Success (No Content)\" };\n                    }\n                    break;\n                default:\n                    throw new Error(`Unsupported method: ${method}`);\n            }\n        } // --- Azure Resource Management Logic (using direct fetch) ---\n        else { // apiType === 'azure'\n            if (!authManager) {\n                throw new Error(\"Auth manager not initialized\");\n            }\n            determinedUrl = \"https://management.azure.com\"; // For error reporting\n            // Acquire token for Azure RM\n            const azureCredential = authManager.getAzureCredential();\n            const tokenResponse = await azureCredential.getToken(\"https://management.azure.com/.default\");\n            if (!tokenResponse || !tokenResponse.token) {\n                throw new Error(\"Failed to acquire Azure access token\");\n            }\n            // Construct the URL (similar to previous implementation)\n            let url = determinedUrl;\n            if (subscriptionId) {\n                url += `/subscriptions/${subscriptionId}`;\n            }\n            url += path;\n            if (!apiVersion) {\n                throw new Error(\"API version is required for Azure Resource Management queries\");\n            }\n            const urlParams = new URLSearchParams({ 'api-version': apiVersion });\n            if (queryParams) {\n                for (const [key, value] of Object.entries(queryParams)) {\n                    urlParams.append(String(key), String(value));\n                }\n            }\n            url += `?${urlParams.toString()}`;\n            // Prepare request options\n            const headers = {\n                'Authorization': `Bearer ${tokenResponse.token}`,\n                'Content-Type': 'application/json'\n            };\n            const requestOptions = {\n                method: method.toUpperCase(),\n                headers: headers\n            };\n            if ([\"POST\", \"PUT\", \"PATCH\"].includes(method.toUpperCase())) {\n                requestOptions.body = body ? JSON.stringify(body) : JSON.stringify({});\n            }\n            // --- Pagination Logic for Azure RM (Manual Fetch) ---\n            if (fetchAll && method === 'get') {\n                logger.info(`Fetching all pages for Azure RM starting from: ${url}`);\n                let allValues = [];\n                let currentUrl = url;\n                while (currentUrl) {\n                    logger.info(`Fetching Azure RM page: ${currentUrl}`);\n                    // Re-acquire token for each page (Azure tokens might expire)\n                    const azureCredential = authManager.getAzureCredential();\n                    const currentPageTokenResponse = await azureCredential.getToken(\"https://management.azure.com/.default\");\n                    if (!currentPageTokenResponse || !currentPageTokenResponse.token) {\n                        throw new Error(\"Failed to acquire Azure access token during pagination\");\n                    }\n                    const currentPageHeaders = { ...headers, 'Authorization': `Bearer ${currentPageTokenResponse.token}` };\n                    const currentPageRequestOptions = { method: 'GET', headers: currentPageHeaders };\n                    const pageResponse = await fetch(currentUrl, currentPageRequestOptions);\n                    const pageText = await pageResponse.text();\n                    let pageData;\n                    try {\n                        pageData = pageText ? JSON.parse(pageText) : {};\n                    }\n                    catch (e) {\n                        logger.error(`Failed to parse JSON from Azure RM page: ${currentUrl}`, pageText);\n                        pageData = { rawResponse: pageText };\n                    }\n                    if (!pageResponse.ok) {\n                        logger.error(`API error on Azure RM page ${currentUrl}:`, pageData);\n                        throw new Error(`API error (${pageResponse.status}) during Azure RM pagination on ${currentUrl}: ${JSON.stringify(pageData)}`);\n                    }\n                    if (pageData.value && Array.isArray(pageData.value)) {\n                        allValues = allValues.concat(pageData.value);\n                    }\n                    else if (currentUrl === url && !pageData.nextLink) {\n                        allValues.push(pageData);\n                    }\n                    else if (currentUrl !== url) {\n                        logger.info(`[Warning] Azure RM response from ${currentUrl} did not contain a 'value' array.`);\n                    }\n                    currentUrl = pageData.nextLink || null; // Azure uses nextLink\n                }\n                responseData = { allValues: allValues };\n                logger.info(`Finished fetching all Azure RM pages. Total items: ${allValues.length}`);\n            }\n            else {\n                // Single page fetch for Azure RM\n                logger.info(`Fetching single page for Azure RM: ${url}`);\n                const apiResponse = await fetch(url, requestOptions);\n                const responseText = await apiResponse.text();\n                try {\n                    responseData = responseText ? JSON.parse(responseText) : {};\n                }\n                catch (e) {\n                    logger.error(`Failed to parse JSON from single Azure RM page: ${url}`, responseText);\n                    responseData = { rawResponse: responseText };\n                }\n                if (!apiResponse.ok) {\n                    logger.error(`API error for Azure RM ${method} ${path}:`, responseData);\n                    throw new Error(`API error (${apiResponse.status}) for Azure RM: ${JSON.stringify(responseData)}`);\n                }\n            }\n        }\n        // --- Format and Return Result ---\n        // For all requests, format as text\n        let resultText = `Result for ${apiType} API (${apiType === 'graph' ? effectiveGraphApiVersion : apiVersion}) - ${method} ${path}:\\n\\n`;\n        resultText += JSON.stringify(responseData, null, 2); // responseData already contains the correct structure for fetchAll Graph case\n        // Add pagination note if applicable (only for single page GET)\n        if (!fetchAll && method === 'get') {\n            const nextLinkKey = apiType === 'graph' ? '@odata.nextLink' : 'nextLink';\n            if (responseData && responseData[nextLinkKey]) { // Added check for responseData existence\n                resultText += `\\n\\nNote: More results are available. To retrieve all pages, add the parameter 'fetchAll: true' to your request.`;\n            }\n        }\n        return {\n            content: [{ type: \"text\", text: resultText }],\n        };\n    }\n    catch (error) {\n        logger.error(`Error in Lokka-Microsoft tool (apiType: ${apiType}, path: ${path}, method: ${method}):`, error); // Added more context to error log\n        // Try to determine the base URL even in case of error\n        if (!determinedUrl) {\n            determinedUrl = apiType === 'graph'\n                ? `https://graph.microsoft.com/${effectiveGraphApiVersion}`\n                : \"https://management.azure.com\";\n        }\n        // Include error body if available from Graph SDK error\n        const errorBody = error.body ? (typeof error.body === 'string' ? error.body : JSON.stringify(error.body)) : 'N/A';\n        return {\n            content: [{\n                    type: \"text\",\n                    text: JSON.stringify({\n                        error: error instanceof Error ? error.message : String(error),\n                        statusCode: error.statusCode || 'N/A', // Include status code if available from SDK error\n                        errorBody: errorBody,\n                        attemptedBaseUrl: determinedUrl\n                    }),\n                }],\n            isError: true\n        };\n    }\n});\n// Add token management tools\nserver.tool(\"set-access-token\", \"Set or update the access token for Microsoft Graph authentication. Use this when the MCP Client has obtained a fresh token through interactive authentication.\", {\n    accessToken: z.string().describe(\"The access token obtained from Microsoft Graph authentication\"),\n    expiresOn: z.string().optional().describe(\"Token expiration time in ISO format (optional, defaults to 1 hour from now)\")\n}, async ({ accessToken, expiresOn }) => {\n    try {\n        const expirationDate = expiresOn ? new Date(expiresOn) : undefined;\n        if (authManager?.getAuthMode() === AuthMode.ClientProvidedToken) {\n            authManager.updateAccessToken(accessToken, expirationDate);\n            // Reinitialize the Graph client with the new token\n            const authProvider = authManager.getGraphAuthProvider();\n            graphClient = Client.initWithMiddleware({\n                authProvider: authProvider,\n            });\n            return {\n                content: [{\n                        type: \"text\",\n                        text: \"Access token updated successfully. You can now make Microsoft Graph requests on behalf of the authenticated user.\"\n                    }],\n            };\n        }\n        else {\n            return {\n                content: [{\n                        type: \"text\",\n                        text: \"Error: MCP Server is not configured for client-provided token authentication. Set USE_CLIENT_TOKEN=true in environment variables.\"\n                    }],\n                isError: true\n            };\n        }\n    }\n    catch (error) {\n        logger.error(\"Error setting access token:\", error);\n        return {\n            content: [{\n                    type: \"text\",\n                    text: `Error setting access token: ${error.message}`\n                }],\n            isError: true\n        };\n    }\n});\nserver.tool(\"get-auth-status\", \"Check the current authentication status and mode of the MCP Server and also returns the current graph permission scopes of the access token for the current session.\", {}, async () => {\n    try {\n        const authMode = authManager?.getAuthMode() || \"Not initialized\";\n        const isReady = authManager !== null;\n        const tokenStatus = authManager ? await authManager.getTokenStatus() : { isExpired: false };\n        return {\n            content: [{\n                    type: \"text\",\n                    text: JSON.stringify({\n                        authMode,\n                        isReady,\n                        supportsTokenUpdates: authMode === AuthMode.ClientProvidedToken,\n                        tokenStatus: tokenStatus,\n                        timestamp: new Date().toISOString()\n                    }, null, 2)\n                }],\n        };\n    }\n    catch (error) {\n        return {\n            content: [{\n                    type: \"text\",\n                    text: `Error checking auth status: ${error.message}`\n                }],\n            isError: true\n        };\n    }\n});\n// Add tool for requesting additional Graph permissions\nserver.tool(\"add-graph-permission\", \"Request additional Microsoft Graph permission scopes by performing a fresh interactive sign-in. This tool only works in interactive authentication mode and should be used if any Graph API call returns permissions related errors.\", {\n    scopes: z.array(z.string()).describe(\"Array of Microsoft Graph permission scopes to request (e.g., ['User.Read', 'Mail.ReadWrite', 'Directory.Read.All'])\")\n}, async ({ scopes }) => {\n    try {\n        // Check if we're in interactive mode\n        if (!authManager || authManager.getAuthMode() !== AuthMode.Interactive) {\n            const currentMode = authManager?.getAuthMode() || \"Not initialized\";\n            const clientId = process.env.CLIENT_ID;\n            let errorMessage = `Error: add-graph-permission tool is only available in interactive authentication mode. Current mode: ${currentMode}.\\n\\n`;\n            if (currentMode === AuthMode.ClientCredentials) {\n                errorMessage += `📋 To add permissions in Client Credentials mode:\\n`;\n                errorMessage += `1. Open the Microsoft Entra admin center (https://entra.microsoft.com)\\n`;\n                errorMessage += `2. Navigate to Applications > App registrations\\n`;\n                errorMessage += `3. Find your application${clientId ? ` (Client ID: ${clientId})` : ''}\\n`;\n                errorMessage += `4. Go to API permissions\\n`;\n                errorMessage += `5. Click \"Add a permission\" and select Microsoft Graph\\n`;\n                errorMessage += `6. Choose \"Application permissions\" and add the required scopes:\\n`;\n                errorMessage += `   ${scopes.map(scope => `• ${scope}`).join('\\n   ')}\\n`;\n                errorMessage += `7. Click \"Grant admin consent\" to approve the permissions\\n`;\n                errorMessage += `8. Restart the MCP server to use the new permissions`;\n            }\n            else if (currentMode === AuthMode.ClientProvidedToken) {\n                errorMessage += `📋 To add permissions in Client Provided Token mode:\\n`;\n                errorMessage += `1. Obtain a new access token that includes the required scopes:\\n`;\n                errorMessage += `   ${scopes.map(scope => `• ${scope}`).join('\\n   ')}\\n`;\n                errorMessage += `2. When obtaining the token, ensure these scopes are included in the consent prompt\\n`;\n                errorMessage += `3. Use the set-access-token tool to update the server with the new token\\n`;\n                errorMessage += `4. The new token will include the additional permissions`;\n            }\n            else {\n                errorMessage += `To use interactive permission requests, set USE_INTERACTIVE=true in environment variables and restart the server.`;\n            }\n            return {\n                content: [{\n                        type: \"text\",\n                        text: errorMessage\n                    }],\n                isError: true\n            };\n        }\n        // Validate scopes array\n        if (!scopes || scopes.length === 0) {\n            return {\n                content: [{\n                        type: \"text\",\n                        text: \"Error: At least one permission scope must be specified.\"\n                    }],\n                isError: true\n            };\n        }\n        // Validate scope format (basic validation)\n        const invalidScopes = scopes.filter(scope => !scope.includes('.') || scope.trim() !== scope);\n        if (invalidScopes.length > 0) {\n            return {\n                content: [{\n                        type: \"text\",\n                        text: `Error: Invalid scope format detected: ${invalidScopes.join(', ')}. Scopes should be in format like 'User.Read' or 'Mail.ReadWrite'.`\n                    }],\n                isError: true\n            };\n        }\n        logger.info(`Requesting additional Graph permissions: ${scopes.join(', ')}`);\n        // Get current configuration with defaults for interactive auth\n        const tenantId = process.env.TENANT_ID || LokkaDefaultTenantId;\n        const clientId = process.env.CLIENT_ID || LokkaClientId;\n        const redirectUri = process.env.REDIRECT_URI || LokkaDefaultRedirectUri;\n        logger.info(`Using tenant ID: ${tenantId}, client ID: ${clientId} for interactive authentication`);\n        // Create a new interactive credential with the requested scopes\n        const { InteractiveBrowserCredential, DeviceCodeCredential } = await import(\"@azure/identity\");\n        // Clear any existing auth manager to force fresh authentication\n        authManager = null;\n        graphClient = null;\n        // Request token with the new scopes - this will trigger interactive authentication\n        const scopeString = scopes.map(scope => `https://graph.microsoft.com/${scope}`).join(' ');\n        logger.info(`Requesting fresh token with scopes: ${scopeString}`);\n        console.log(`\\n🔐 Requesting Additional Graph Permissions:`);\n        console.log(`Scopes: ${scopes.join(', ')}`);\n        console.log(`You will be prompted to sign in to grant these permissions.\\n`);\n        let newCredential;\n        let tokenResponse;\n        try {\n            // Try Interactive Browser first - create fresh instance each time\n            newCredential = new InteractiveBrowserCredential({\n                tenantId: tenantId,\n                clientId: clientId,\n                redirectUri: redirectUri,\n            });\n            // Request token immediately after creating credential\n            tokenResponse = await newCredential.getToken(scopeString);\n        }\n        catch (error) {\n            // Fallback to Device Code flow\n            logger.info(\"Interactive browser failed, falling back to device code flow\");\n            newCredential = new DeviceCodeCredential({\n                tenantId: tenantId,\n                clientId: clientId,\n                userPromptCallback: (info) => {\n                    console.log(`\\n🔐 Additional Permissions Required:`);\n                    console.log(`Please visit: ${info.verificationUri}`);\n                    console.log(`And enter code: ${info.userCode}`);\n                    console.log(`Requested scopes: ${scopes.join(', ')}\\n`);\n                    return Promise.resolve();\n                },\n            });\n            // Request token with device code credential\n            tokenResponse = await newCredential.getToken(scopeString);\n        }\n        if (!tokenResponse) {\n            return {\n                content: [{\n                        type: \"text\",\n                        text: \"Error: Failed to acquire access token with the requested scopes. Please check your permissions and try again.\"\n                    }],\n                isError: true\n            };\n        }\n        // Create a completely new auth manager instance with the updated credential\n        const authConfig = {\n            mode: AuthMode.Interactive,\n            tenantId,\n            clientId,\n            redirectUri\n        };\n        // Create a new auth manager instance\n        authManager = new AuthManager(authConfig);\n        // Manually set the credential to our new one with the additional scopes\n        authManager.credential = newCredential;\n        // DO NOT call initialize() as it might interfere with our fresh token\n        // Instead, directly create the Graph client with the new credential\n        const authProvider = authManager.getGraphAuthProvider();\n        graphClient = Client.initWithMiddleware({\n            authProvider: authProvider,\n        });\n        // Get the token status to show the new scopes\n        const tokenStatus = await authManager.getTokenStatus();\n        logger.info(`Successfully acquired fresh token with additional scopes: ${scopes.join(', ')}`);\n        return {\n            content: [{\n                    type: \"text\",\n                    text: JSON.stringify({\n                        message: \"Successfully acquired additional Microsoft Graph permissions with fresh authentication\",\n                        requestedScopes: scopes,\n                        tokenStatus: tokenStatus,\n                        note: \"A fresh sign-in was performed to ensure the new permissions are properly granted\",\n                        timestamp: new Date().toISOString()\n                    }, null, 2)\n                }],\n        };\n    }\n    catch (error) {\n        logger.error(\"Error requesting additional Graph permissions:\", error);\n        return {\n            content: [{\n                    type: \"text\",\n                    text: `Error requesting additional permissions: ${error.message}`\n                }],\n            isError: true\n        };\n    }\n});\n// Start the server with stdio transport\nasync function main() {\n    // Determine authentication mode based on environment variables\n    const useCertificate = process.env.USE_CERTIFICATE === 'true';\n    const useInteractive = process.env.USE_INTERACTIVE === 'true';\n    const useClientToken = process.env.USE_CLIENT_TOKEN === 'true';\n    const initialAccessToken = process.env.ACCESS_TOKEN;\n    let authMode;\n    // Ensure only one authentication mode is enabled at a time\n    const enabledModes = [\n        useClientToken,\n        useInteractive,\n        useCertificate\n    ].filter(Boolean);\n    if (enabledModes.length > 1) {\n        throw new Error(\"Multiple authentication modes enabled. Please enable only one of USE_CLIENT_TOKEN, USE_INTERACTIVE, or USE_CERTIFICATE.\");\n    }\n    if (useClientToken) {\n        authMode = AuthMode.ClientProvidedToken;\n        if (!initialAccessToken) {\n            logger.info(\"Client token mode enabled but no initial token provided. Token must be set via set-access-token tool.\");\n        }\n    }\n    else if (useInteractive) {\n        authMode = AuthMode.Interactive;\n    }\n    else if (useCertificate) {\n        authMode = AuthMode.Certificate;\n    }\n    else {\n        // Check if we have client credentials environment variables\n        const hasClientCredentials = process.env.TENANT_ID && process.env.CLIENT_ID && process.env.CLIENT_SECRET;\n        if (hasClientCredentials) {\n            authMode = AuthMode.ClientCredentials;\n        }\n        else {\n            // Default to interactive mode for better user experience\n            authMode = AuthMode.Interactive;\n            logger.info(\"No authentication mode specified and no client credentials found. Defaulting to interactive mode.\");\n        }\n    }\n    logger.info(`Starting with authentication mode: ${authMode}`);\n    // Get tenant ID and client ID with defaults only for interactive mode\n    let tenantId;\n    let clientId;\n    if (authMode === AuthMode.Interactive) {\n        // Interactive mode can use defaults\n        tenantId = process.env.TENANT_ID || LokkaDefaultTenantId;\n        clientId = process.env.CLIENT_ID || LokkaClientId;\n        logger.info(`Interactive mode using tenant ID: ${tenantId}, client ID: ${clientId}`);\n    }\n    else {\n        // All other modes require explicit values from environment variables\n        tenantId = process.env.TENANT_ID;\n        clientId = process.env.CLIENT_ID;\n    }\n    const clientSecret = process.env.CLIENT_SECRET;\n    const certificatePath = process.env.CERTIFICATE_PATH;\n    const certificatePassword = process.env.CERTIFICATE_PASSWORD; // optional\n    // Validate required configuration\n    if (authMode === AuthMode.ClientCredentials) {\n        if (!tenantId || !clientId || !clientSecret) {\n            throw new Error(\"Client credentials mode requires explicit TENANT_ID, CLIENT_ID, and CLIENT_SECRET environment variables\");\n        }\n    }\n    else if (authMode === AuthMode.Certificate) {\n        if (!tenantId || !clientId || !certificatePath) {\n            throw new Error(\"Certificate mode requires explicit TENANT_ID, CLIENT_ID, and CERTIFICATE_PATH environment variables\");\n        }\n    }\n    // Note: Client token mode can start without a token and receive it later\n    const authConfig = {\n        mode: authMode,\n        tenantId,\n        clientId,\n        clientSecret,\n        accessToken: initialAccessToken,\n        redirectUri: process.env.REDIRECT_URI,\n        certificatePath,\n        certificatePassword\n    };\n    authManager = new AuthManager(authConfig);\n    // Only initialize if we have required config (for client token mode, we can start without a token)\n    if (authMode !== AuthMode.ClientProvidedToken || initialAccessToken) {\n        await authManager.initialize();\n        // Initialize Graph Client\n        const authProvider = authManager.getGraphAuthProvider();\n        graphClient = Client.initWithMiddleware({\n            authProvider: authProvider,\n        });\n        logger.info(`Authentication initialized successfully using ${authMode} mode`);\n    }\n    else {\n        logger.info(\"Started in client token mode. Use set-access-token tool to provide authentication token.\");\n    }\n    const transport = new StdioServerTransport();\n    await server.connect(transport);\n}\nmain().catch((error) => {\n    console.error(\"Fatal error in main():\", error);\n    logger.error(\"Fatal error in main()\", error);\n    process.exit(1);\n});\n"
  },
  {
    "path": "src/mcp/package.json",
    "content": "{\n  \"name\": \"@merill/lokka\",\n  \"version\": \"0.3.0\",\n  \"description\": \"Lokka is a Model Context Protocol (MCP) server for Microsoft Graph.\",\n  \"license\": \"MIT\",\n  \"author\": \"Merill\",\n  \"homepage\": \"https://lokka.dev\",\n  \"bugs\": \"https://github.com/merill/lokka/issues\",\n  \"main\": \"main.js\",\n  \"type\": \"module\",\n  \"keywords\": [\n    \"mcp\",\n    \"graph\",\n    \"microsoft\",\n    \"graph\",\n    \"model\",\n    \"context\",\n    \"protocol\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/merill/lokka.git\",\n    \"directory\": \"src/mcp\"\n  },\n  \"bin\": {\n    \"lokka\": \"build/main.js\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc\",\n    \"build:unix\": \"tsc && chmod 755 build/main.js\",\n    \"demo:token\": \"node build/demo-token-auth.js\",\n    \"test:token\": \"node build/test-token-auth.js\",\n    \"test:simple\": \"node build/simple-token-test.js\",\n    \"test:live\": \"node build/live-test.js\",\n    \"start\": \"node build/main.js\"\n  },\n  \"files\": [\n    \"build\"\n  ],\n  \"dependencies\": {\n    \"@azure/identity\": \"^4.3.0\",\n    \"@microsoft/microsoft-graph-client\": \"^3.0.7\",\n    \"@modelcontextprotocol/sdk\": \"^1.7.0\",\n    \"@types/jsonwebtoken\": \"^9.0.10\",\n    \"isomorphic-fetch\": \"^3.0.0\",\n    \"jsonwebtoken\": \"^9.0.2\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@types/isomorphic-fetch\": \"^0.0.39\",\n    \"@types/node\": \"^22.13.17\",\n    \"typescript\": \"^5.8.2\"\n  }\n}\n"
  },
  {
    "path": "src/mcp/src/auth.ts",
    "content": "import { AccessToken, TokenCredential, ClientSecretCredential, ClientCertificateCredential, InteractiveBrowserCredential, DeviceCodeCredential, DeviceCodeInfo } from \"@azure/identity\";\nimport { AuthenticationProvider } from \"@microsoft/microsoft-graph-client\";\nimport jwt from \"jsonwebtoken\";\nimport { logger } from \"./logger.js\";\nimport { LokkaClientId, LokkaDefaultTenantId, LokkaDefaultRedirectUri } from \"./constants.js\";\n\n// Constants\nconst ONE_HOUR_IN_MS = 60 * 60 * 1000; // One hour in milliseconds\n\n// Helper function to parse JWT and extract scopes\nfunction parseJwtScopes(token: string): string[] {\n  try {\n    // Decode JWT without verifying signature (we trust the token from Azure Identity)\n    const decoded = jwt.decode(token) as any;\n    \n    if (!decoded || typeof decoded !== 'object') {\n      logger.info(\"Failed to decode JWT token\");\n      return [];\n    }\n\n    // Extract scopes from the 'scp' claim (space-separated string)\n    const scopesString = decoded.scp;\n    if (typeof scopesString === 'string') {\n      return scopesString.split(' ').filter(scope => scope.length > 0);\n    }\n\n    // Some tokens might have roles instead of scopes\n    const roles = decoded.roles;\n    if (Array.isArray(roles)) {\n      return roles;\n    }\n\n    logger.info(\"No scopes found in JWT token\");\n    return [];\n  } catch (error) {\n    logger.error(\"Error parsing JWT token for scopes\", error);\n    return [];\n  }\n}\n\n// Simple authentication provider that works with Azure Identity TokenCredential\nexport class TokenCredentialAuthProvider implements AuthenticationProvider {\n  private credential: TokenCredential;\n\n  constructor(credential: TokenCredential) {\n    this.credential = credential;\n  }\n\n  async getAccessToken(): Promise<string> {\n    const token = await this.credential.getToken(\"https://graph.microsoft.com/.default\");\n    if (!token) {\n      throw new Error(\"Failed to acquire access token\");\n    }\n    return token.token;\n  }\n}\n\nexport interface TokenBasedCredential extends TokenCredential {\n  getToken(scopes: string | string[]): Promise<AccessToken | null>;\n}\n\nexport class ClientProvidedTokenCredential implements TokenBasedCredential {\n  private accessToken: string | undefined;\n  private expiresOn: Date | undefined;\n  constructor(accessToken?: string, expiresOn?: Date) {\n    if (accessToken) {\n      this.accessToken = accessToken;\n      this.expiresOn = expiresOn || new Date(Date.now() + ONE_HOUR_IN_MS); // Default 1 hour\n    } else {\n      this.expiresOn = new Date(0); // Set to epoch to indicate no valid token\n    }\n  }\n  async getToken(scopes: string | string[]): Promise<AccessToken | null> {\n    if (!this.accessToken || !this.expiresOn || this.expiresOn <= new Date()) {\n      logger.error(\"Access token is not available or has expired\");\n      return null;\n    }\n\n    return {\n      token: this.accessToken,\n      expiresOnTimestamp: this.expiresOn.getTime()\n    };\n  }  updateToken(accessToken: string, expiresOn?: Date): void {\n    this.accessToken = accessToken;\n    this.expiresOn = expiresOn || new Date(Date.now() + ONE_HOUR_IN_MS);\n    logger.info(\"Access token updated successfully\");\n  }\n  isExpired(): boolean {\n    return !this.expiresOn || this.expiresOn <= new Date();\n  }\n\n  getExpirationTime(): Date {\n    return this.expiresOn || new Date(0);\n  }\n\n  // Getter for access token (for internal use by AuthManager)\n  getAccessToken(): string | undefined {\n    return this.accessToken;\n  }\n}\n\nexport enum AuthMode {\n  ClientCredentials = \"client_credentials\",\n  ClientProvidedToken = \"client_provided_token\", \n  Interactive = \"interactive\",\n  Certificate = \"certificate\"\n}\n\nexport interface AuthConfig {\n  mode: AuthMode;\n  tenantId?: string;\n  clientId?: string;\n  clientSecret?: string;\n  accessToken?: string;\n  expiresOn?: Date;\n  redirectUri?: string;\n  certificatePath?: string;\n  certificatePassword?: string;\n}\n\nexport class AuthManager {\n  private credential: TokenCredential | null = null;\n  private config: AuthConfig;\n\n  constructor(config: AuthConfig) {\n    this.config = config;\n  }\n\n  async initialize(): Promise<void> {\n    switch (this.config.mode) {\n      case AuthMode.ClientCredentials:\n        if (!this.config.tenantId || !this.config.clientId || !this.config.clientSecret) {\n          throw new Error(\"Client credentials mode requires tenantId, clientId, and clientSecret\");\n        }\n        logger.info(\"Initializing Client Credentials authentication\");\n        this.credential = new ClientSecretCredential(\n          this.config.tenantId,\n          this.config.clientId,\n          this.config.clientSecret\n        );\n        break;\n\n      case AuthMode.ClientProvidedToken:\n        logger.info(\"Initializing Client Provided Token authentication\");\n        this.credential = new ClientProvidedTokenCredential(\n          this.config.accessToken,\n          this.config.expiresOn\n        );\n        break;\n        \n      case AuthMode.Certificate:\n        if (!this.config.tenantId || !this.config.clientId || !this.config.certificatePath) {\n          throw new Error(\"Certificate mode requires tenantId, clientId, and certificatePath\");\n        }\n        logger.info(\"Initializing Certificate authentication\");\n        this.credential = new ClientCertificateCredential(this.config.tenantId, this.config.clientId, {\n          certificatePath: this.config.certificatePath,\n          certificatePassword: this.config.certificatePassword\n        });\n        break;\n\n      case AuthMode.Interactive:\n        // Use defaults if not provided\n        const tenantId = this.config.tenantId || LokkaDefaultTenantId;\n        const clientId = this.config.clientId || LokkaClientId;\n        \n        logger.info(`Initializing Interactive authentication with tenant ID: ${tenantId}, client ID: ${clientId}`);\n        \n        try {\n          // Try Interactive Browser first\n          this.credential = new InteractiveBrowserCredential({\n            tenantId: tenantId,\n            clientId: clientId,\n            redirectUri: this.config.redirectUri || LokkaDefaultRedirectUri,\n          });\n        } catch (error) {\n          // Fallback to Device Code flow\n          logger.info(\"Interactive browser failed, falling back to device code flow\");\n          this.credential = new DeviceCodeCredential({\n            tenantId: tenantId,\n            clientId: clientId,\n            userPromptCallback: (info: DeviceCodeInfo) => {\n              console.log(`\\n🔐 Authentication Required:`);\n              console.log(`Please visit: ${info.verificationUri}`);\n              console.log(`And enter code: ${info.userCode}\\n`);\n              return Promise.resolve();\n            },\n          });\n        }\n        break;\n\n      default:\n        throw new Error(`Unsupported authentication mode: ${this.config.mode}`);\n    }\n\n    // Test the credential\n    await this.testCredential();\n  }\n\n  updateAccessToken(accessToken: string, expiresOn?: Date): void {\n    if (this.config.mode === AuthMode.ClientProvidedToken && this.credential instanceof ClientProvidedTokenCredential) {\n      this.credential.updateToken(accessToken, expiresOn);\n    } else {\n      throw new Error(\"Token update only supported in client provided token mode\");\n    }\n  }\n  private async testCredential(): Promise<void> {\n    if (!this.credential) {\n      throw new Error(\"Credential not initialized\");\n    }\n\n    // Skip testing if ClientProvidedToken mode has no initial token\n    if (this.config.mode === AuthMode.ClientProvidedToken && !this.config.accessToken) {\n      logger.info(\"Skipping initial credential test as no token was provided at startup.\");\n      return;\n    }\n\n    try {\n      const token = await this.credential.getToken(\"https://graph.microsoft.com/.default\");\n      if (!token) {\n        throw new Error(\"Failed to acquire token\");\n      }\n      logger.info(\"Authentication successful\");\n    } catch (error) {\n      logger.error(\"Authentication test failed\", error);\n      throw error;\n    }\n  }\n  getGraphAuthProvider(): TokenCredentialAuthProvider {\n    if (!this.credential) {\n      throw new Error(\"Authentication not initialized\");\n    }\n\n    return new TokenCredentialAuthProvider(this.credential);\n  }\n\n  getAzureCredential(): TokenCredential {\n    if (!this.credential) {\n      throw new Error(\"Authentication not initialized\");\n    }\n    return this.credential;\n  }\n\n  getAuthMode(): AuthMode {\n    return this.config.mode;\n  }\n\n  isClientCredentials(): boolean {\n    return this.config.mode === AuthMode.ClientCredentials;\n  }\n\n  isClientProvidedToken(): boolean {\n    return this.config.mode === AuthMode.ClientProvidedToken;\n  }\n\n  isInteractive(): boolean {\n    return this.config.mode === AuthMode.Interactive;\n  }\n\n  async getTokenStatus(): Promise<{ isExpired: boolean; expiresOn?: Date; scopes?: string[] }> {\n    if (this.credential instanceof ClientProvidedTokenCredential) {\n      const tokenStatus = {\n        isExpired: this.credential.isExpired(),\n        expiresOn: this.credential.getExpirationTime()\n      };\n\n      // If we have a valid token, parse it to extract scopes\n      if (!tokenStatus.isExpired) {\n        const accessToken = this.credential.getAccessToken();\n        if (accessToken) {\n          try {\n            const scopes = parseJwtScopes(accessToken);\n            return {\n              ...tokenStatus,\n              scopes: scopes\n            };\n          } catch (error) {\n            logger.error(\"Error parsing token scopes in getTokenStatus\", error);\n            return tokenStatus;\n          }\n        }\n      }\n\n      return tokenStatus;\n    } else if (this.credential) {\n      // For other credential types, try to get a fresh token and parse it\n      try {\n        const accessToken = await this.credential.getToken(\"https://graph.microsoft.com/.default\");\n        if (accessToken && accessToken.token) {\n          const scopes = parseJwtScopes(accessToken.token);\n          return {\n            isExpired: false,\n            expiresOn: new Date(accessToken.expiresOnTimestamp),\n            scopes: scopes\n          };\n        }\n      } catch (error) {\n        logger.error(\"Error getting token for scope parsing\", error);\n      }\n    }\n    \n    return { isExpired: false };\n  }\n}\n"
  },
  {
    "path": "src/mcp/src/constants.ts",
    "content": "// Shared constants for the Lokka MCP Server\n\nexport const LokkaClientId = \"a9bac4c3-af0d-4292-9453-9da89e390140\";\nexport const LokkaDefaultTenantId = \"common\";\nexport const LokkaDefaultRedirectUri = \"http://localhost:3000\";\n\n// Default Graph API version based on USE_GRAPH_BETA environment variable\nexport const getDefaultGraphApiVersion = (): \"v1.0\" | \"beta\" => {\n  return process.env.USE_GRAPH_BETA !== 'false' ? \"beta\" : \"v1.0\";\n};\n"
  },
  {
    "path": "src/mcp/src/logger.ts",
    "content": "import { appendFileSync } from \"fs\";\nimport { join } from \"path\";\n\nconst LOG_FILE = join(\n  import.meta.dirname,\n  \"mcp-server.log\",\n);\n\nfunction formatMessage(\n  level: string,\n  message: string,\n  data?: unknown,\n): string {\n  const timestamp = new Date().toISOString();\n  const dataStr = data\n    ? `\\n${JSON.stringify(data, null, 2)}`\n    : \"\";\n  return `[${timestamp}] [${level}] ${message}${dataStr}\\n`;\n}\n\nexport const logger = {\n  info(message: string, data?: unknown) {\n    const logMessage = formatMessage(\n      \"INFO\",\n      message,\n      data,\n    );\n    appendFileSync(LOG_FILE, logMessage);\n  },\n\n  error(message: string, error?: unknown) {\n    const logMessage = formatMessage(\n      \"ERROR\",\n      message,\n      error,\n    );\n    appendFileSync(LOG_FILE, logMessage);\n  },\n\n  // debug(message: string, data?: unknown) {\n  //   const logMessage = formatMessage(\n  //     \"DEBUG\",\n  //     message,\n  //     data,\n  //   );\n  //   appendFileSync(LOG_FILE, logMessage);\n  // },\n};"
  },
  {
    "path": "src/mcp/src/main.ts",
    "content": "#!/usr/bin/env node\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\nimport { Client, PageIterator, PageCollection } from \"@microsoft/microsoft-graph-client\";\nimport fetch from 'isomorphic-fetch'; // Required polyfill for Graph client\nimport { logger } from \"./logger.js\";\nimport { AuthManager, AuthConfig, AuthMode } from \"./auth.js\";\nimport { LokkaClientId, LokkaDefaultTenantId, LokkaDefaultRedirectUri, getDefaultGraphApiVersion } from \"./constants.js\";\n\n// Set up global fetch for the Microsoft Graph client\n(global as any).fetch = fetch;\n\n// Create server instance\nconst server = new McpServer({\n  name: \"Lokka-Microsoft\",\n  version: \"0.2.0\", // Updated version for token-based auth support\n});\n\nlogger.info(\"Starting Lokka Multi-Microsoft API MCP Server (v0.2.0 - Token-Based Auth Support)\");\n\n// Initialize authentication and clients\nlet authManager: AuthManager | null = null;\nlet graphClient: Client | null = null;\n\n// Check USE_GRAPH_BETA environment variable\nconst useGraphBeta = process.env.USE_GRAPH_BETA !== 'false'; // Default to true unless explicitly set to 'false'\nconst defaultGraphApiVersion = getDefaultGraphApiVersion();\n\nlogger.info(`Graph API default version: ${defaultGraphApiVersion} (USE_GRAPH_BETA=${process.env.USE_GRAPH_BETA || 'undefined'})`);\n\nserver.tool(\n  \"Lokka-Microsoft\",\n  \"A versatile tool to interact with Microsoft APIs including Microsoft Graph (Entra) and Azure Resource Management. IMPORTANT: For Graph API GET requests using advanced query parameters ($filter, $count, $search, $orderby), you are ADVISED to set 'consistencyLevel: \\\"eventual\\\"'.\",\n  {\n    apiType: z.enum([\"graph\", \"azure\"]).describe(\"Type of Microsoft API to query. Options: 'graph' for Microsoft Graph (Entra) or 'azure' for Azure Resource Management.\"),\n    path: z.string().describe(\"The Azure or Graph API URL path to call (e.g. '/users', '/groups', '/subscriptions')\"),\n    method: z.enum([\"get\", \"post\", \"put\", \"patch\", \"delete\"]).describe(\"HTTP method to use\"),\n    apiVersion: z.string().optional().describe(\"Azure Resource Management API version (required for apiType Azure)\"),\n    subscriptionId: z.string().optional().describe(\"Azure Subscription ID (for Azure Resource Management).\"),\n    queryParams: z.record(z.string()).optional().describe(\"Query parameters for the request\"),\n    body: z.record(z.string(), z.any()).optional().describe(\"The request body (for POST, PUT, PATCH)\"),\n    graphApiVersion: z.enum([\"v1.0\", \"beta\"]).optional().default(defaultGraphApiVersion as \"v1.0\" | \"beta\").describe(`Microsoft Graph API version to use (default: ${defaultGraphApiVersion})`),\n    fetchAll: z.boolean().optional().default(false).describe(\"Set to true to automatically fetch all pages for list results (e.g., users, groups). Default is false.\"),\n    consistencyLevel: z.string().optional().describe(\"Graph API ConsistencyLevel header. ADVISED to be set to 'eventual' for Graph GET requests using advanced query parameters ($filter, $count, $search, $orderby).\"),\n  },\n  async ({\n    apiType,\n    path,\n    method,\n    apiVersion,\n    subscriptionId,\n    queryParams,\n    body,\n    graphApiVersion,\n    fetchAll,\n    consistencyLevel\n  }: {\n    apiType: \"graph\" | \"azure\";\n    path: string;\n    method: \"get\" | \"post\" | \"put\" | \"patch\" | \"delete\";\n    apiVersion?: string;\n    subscriptionId?: string;\n    queryParams?: Record<string, string>;\n    body?: any;\n    graphApiVersion: \"v1.0\" | \"beta\";\n    fetchAll: boolean;\n    consistencyLevel?: string;\n  }) => {\n    // Override graphApiVersion if USE_GRAPH_BETA is explicitly set to false\n    const effectiveGraphApiVersion = !useGraphBeta ? \"v1.0\" : graphApiVersion;\n    \n    logger.info(`Executing Lokka-Microsoft tool with params: apiType=${apiType}, path=${path}, method=${method}, graphApiVersion=${effectiveGraphApiVersion}, fetchAll=${fetchAll}, consistencyLevel=${consistencyLevel}`);\n    let determinedUrl: string | undefined;\n\n    try {\n      let responseData: any;\n\n      // --- Microsoft Graph Logic ---\n      if (apiType === 'graph') {\n        if (!graphClient) {\n          throw new Error(\"Graph client not initialized\");\n        }\n        determinedUrl = `https://graph.microsoft.com/${effectiveGraphApiVersion}`; // For error reporting\n\n        // Construct the request using the Graph SDK client\n        let request = graphClient.api(path).version(effectiveGraphApiVersion);\n\n        // Add query parameters if provided and not empty\n        if (queryParams && Object.keys(queryParams).length > 0) {\n          request = request.query(queryParams);\n        }\n\n        // Add ConsistencyLevel header if provided\n        if (consistencyLevel) {\n          request = request.header('ConsistencyLevel', consistencyLevel);\n          logger.info(`Added ConsistencyLevel header: ${consistencyLevel}`);\n        }\n\n        // Handle different methods\n        switch (method.toLowerCase()) {\n          case 'get':\n            if (fetchAll) {\n              logger.info(`Fetching all pages for Graph path: ${path}`);\n              // Fetch the first page to get context and initial data\n              const firstPageResponse: PageCollection = await request.get();\n              const odataContext = firstPageResponse['@odata.context']; // Capture context from first page\n              let allItems: any[] = firstPageResponse.value || []; // Initialize with first page's items\n\n              // Callback function to process subsequent pages\n              const callback = (item: any) => {\n                allItems.push(item);\n                return true; // Return true to continue iteration\n              };\n\n              // Create a PageIterator starting from the first response\n              const pageIterator = new PageIterator(graphClient, firstPageResponse, callback);\n\n              // Iterate over all remaining pages\n              await pageIterator.iterate();\n\n              // Construct final response with context and combined values under 'value' key\n              responseData = {\n                '@odata.context': odataContext,\n                value: allItems\n              };\n              logger.info(`Finished fetching all Graph pages. Total items: ${allItems.length}`);\n\n            } else {\n              logger.info(`Fetching single page for Graph path: ${path}`);\n              responseData = await request.get();\n            }\n            break;\n          case 'post':\n            responseData = await request.post(body ?? {});\n            break;\n          case 'put':\n            responseData = await request.put(body ?? {});\n            break;\n          case 'patch':\n            responseData = await request.patch(body ?? {});\n            break;\n          case 'delete':\n            responseData = await request.delete(); // Delete often returns no body or 204\n            // Handle potential 204 No Content response\n            if (responseData === undefined || responseData === null) {\n              responseData = { status: \"Success (No Content)\" };\n            }\n            break;\n          default:\n            throw new Error(`Unsupported method: ${method}`);\n        }\n      }      // --- Azure Resource Management Logic (using direct fetch) ---\n      else { // apiType === 'azure'\n        if (!authManager) {\n          throw new Error(\"Auth manager not initialized\");\n        }\n        determinedUrl = \"https://management.azure.com\"; // For error reporting\n\n        // Acquire token for Azure RM\n        const azureCredential = authManager.getAzureCredential();\n        const tokenResponse = await azureCredential.getToken(\"https://management.azure.com/.default\");\n        if (!tokenResponse || !tokenResponse.token) {\n          throw new Error(\"Failed to acquire Azure access token\");\n        }\n\n        // Construct the URL (similar to previous implementation)\n        let url = determinedUrl;\n        if (subscriptionId) {\n          url += `/subscriptions/${subscriptionId}`;\n        }\n        url += path;\n\n        if (!apiVersion) {\n          throw new Error(\"API version is required for Azure Resource Management queries\");\n        }\n        const urlParams = new URLSearchParams({ 'api-version': apiVersion });\n        if (queryParams) {\n          for (const [key, value] of Object.entries(queryParams)) {\n            urlParams.append(String(key), String(value));\n          }\n        }\n        url += `?${urlParams.toString()}`;\n\n        // Prepare request options\n        const headers: Record<string, string> = {\n          'Authorization': `Bearer ${tokenResponse.token}`,\n          'Content-Type': 'application/json'\n        };\n        const requestOptions: RequestInit = {\n          method: method.toUpperCase(),\n          headers: headers\n        };\n        if ([\"POST\", \"PUT\", \"PATCH\"].includes(method.toUpperCase())) {\n          requestOptions.body = body ? JSON.stringify(body) : JSON.stringify({});\n        }\n\n        // --- Pagination Logic for Azure RM (Manual Fetch) ---\n        if (fetchAll && method === 'get') {\n          logger.info(`Fetching all pages for Azure RM starting from: ${url}`);\n          let allValues: any[] = [];\n          let currentUrl: string | null = url;\n\n          while (currentUrl) {            logger.info(`Fetching Azure RM page: ${currentUrl}`);\n            // Re-acquire token for each page (Azure tokens might expire)\n            const azureCredential = authManager.getAzureCredential();\n            const currentPageTokenResponse = await azureCredential.getToken(\"https://management.azure.com/.default\");\n            if (!currentPageTokenResponse || !currentPageTokenResponse.token) {\n              throw new Error(\"Failed to acquire Azure access token during pagination\");\n            }\n            const currentPageHeaders = { ...headers, 'Authorization': `Bearer ${currentPageTokenResponse.token}` };\n            const currentPageRequestOptions: RequestInit = { method: 'GET', headers: currentPageHeaders };\n\n            const pageResponse = await fetch(currentUrl, currentPageRequestOptions);\n            const pageText = await pageResponse.text();\n            let pageData: any;\n            try {\n              pageData = pageText ? JSON.parse(pageText) : {};\n            } catch (e) {\n              logger.error(`Failed to parse JSON from Azure RM page: ${currentUrl}`, pageText);\n              pageData = { rawResponse: pageText };\n            }\n\n            if (!pageResponse.ok) {\n              logger.error(`API error on Azure RM page ${currentUrl}:`, pageData);\n              throw new Error(`API error (${pageResponse.status}) during Azure RM pagination on ${currentUrl}: ${JSON.stringify(pageData)}`);\n            }\n\n            if (pageData.value && Array.isArray(pageData.value)) {\n              allValues = allValues.concat(pageData.value);\n            } else if (currentUrl === url && !pageData.nextLink) {\n              allValues.push(pageData);\n            } else if (currentUrl !== url) {\n              logger.info(`[Warning] Azure RM response from ${currentUrl} did not contain a 'value' array.`);\n            }\n            currentUrl = pageData.nextLink || null; // Azure uses nextLink\n          }\n          responseData = { allValues: allValues };\n          logger.info(`Finished fetching all Azure RM pages. Total items: ${allValues.length}`);\n        } else {\n          // Single page fetch for Azure RM\n          logger.info(`Fetching single page for Azure RM: ${url}`);\n          const apiResponse = await fetch(url, requestOptions);\n          const responseText = await apiResponse.text();\n          try {\n            responseData = responseText ? JSON.parse(responseText) : {};\n          } catch (e) {\n            logger.error(`Failed to parse JSON from single Azure RM page: ${url}`, responseText);\n            responseData = { rawResponse: responseText };\n          }\n          if (!apiResponse.ok) {\n            logger.error(`API error for Azure RM ${method} ${path}:`, responseData);\n            throw new Error(`API error (${apiResponse.status}) for Azure RM: ${JSON.stringify(responseData)}`);\n          }\n        }\n      }\n\n      // --- Format and Return Result ---\n      // For all requests, format as text\n      let resultText = `Result for ${apiType} API (${apiType === 'graph' ? effectiveGraphApiVersion : apiVersion}) - ${method} ${path}:\\n\\n`;\n      resultText += JSON.stringify(responseData, null, 2); // responseData already contains the correct structure for fetchAll Graph case\n\n      // Add pagination note if applicable (only for single page GET)\n      if (!fetchAll && method === 'get') {\n         const nextLinkKey = apiType === 'graph' ? '@odata.nextLink' : 'nextLink';\n         if (responseData && responseData[nextLinkKey]) { // Added check for responseData existence\n             resultText += `\\n\\nNote: More results are available. To retrieve all pages, add the parameter 'fetchAll: true' to your request.`;\n         }\n      }\n\n      return {\n        content: [{ type: \"text\" as const, text: resultText }],\n      };\n\n    } catch (error: any) {\n      logger.error(`Error in Lokka-Microsoft tool (apiType: ${apiType}, path: ${path}, method: ${method}):`, error); // Added more context to error log\n      // Try to determine the base URL even in case of error\n      if (!determinedUrl) {\n         determinedUrl = apiType === 'graph'\n           ? `https://graph.microsoft.com/${effectiveGraphApiVersion}`\n           : \"https://management.azure.com\";\n      }\n      // Include error body if available from Graph SDK error\n      const errorBody = error.body ? (typeof error.body === 'string' ? error.body : JSON.stringify(error.body)) : 'N/A';\n      return {\n        content: [{\n          type: \"text\",\n          text: JSON.stringify({\n            error: error instanceof Error ? error.message : String(error),\n            statusCode: error.statusCode || 'N/A', // Include status code if available from SDK error\n            errorBody: errorBody,\n            attemptedBaseUrl: determinedUrl\n          }),\n        }],\n        isError: true\n      };\n    }\n  },\n);\n\n// Add token management tools\nserver.tool(\n  \"set-access-token\",\n  \"Set or update the access token for Microsoft Graph authentication. Use this when the MCP Client has obtained a fresh token through interactive authentication.\",\n  {\n    accessToken: z.string().describe(\"The access token obtained from Microsoft Graph authentication\"),\n    expiresOn: z.string().optional().describe(\"Token expiration time in ISO format (optional, defaults to 1 hour from now)\")\n  },\n  async ({ accessToken, expiresOn }) => {\n    try {\n      const expirationDate = expiresOn ? new Date(expiresOn) : undefined;\n      \n      if (authManager?.getAuthMode() === AuthMode.ClientProvidedToken) {\n        authManager.updateAccessToken(accessToken, expirationDate);\n        \n        // Reinitialize the Graph client with the new token\n        const authProvider = authManager.getGraphAuthProvider();\n        graphClient = Client.initWithMiddleware({\n          authProvider: authProvider,\n        });\n        \n        return {\n          content: [{ \n            type: \"text\" as const, \n            text: \"Access token updated successfully. You can now make Microsoft Graph requests on behalf of the authenticated user.\" \n          }],\n        };\n      } else {\n        return {\n          content: [{ \n            type: \"text\" as const, \n            text: \"Error: MCP Server is not configured for client-provided token authentication. Set USE_CLIENT_TOKEN=true in environment variables.\" \n          }],\n          isError: true\n        };\n      }\n    } catch (error: any) {\n      logger.error(\"Error setting access token:\", error);\n      return {\n        content: [{ \n          type: \"text\" as const, \n          text: `Error setting access token: ${error.message}` \n        }],\n        isError: true\n      };\n    }\n  }\n);\n\nserver.tool(\n  \"get-auth-status\",\n  \"Check the current authentication status and mode of the MCP Server and also returns the current graph permission scopes of the access token for the current session.\",\n  {},\n  async () => {\n    try {\n      const authMode = authManager?.getAuthMode() || \"Not initialized\";\n      const isReady = authManager !== null;\n      const tokenStatus = authManager ? await authManager.getTokenStatus() : { isExpired: false };\n      \n      return {\n        content: [{ \n          type: \"text\" as const, \n          text: JSON.stringify({\n            authMode,\n            isReady,\n            supportsTokenUpdates: authMode === AuthMode.ClientProvidedToken,\n            tokenStatus: tokenStatus,\n            timestamp: new Date().toISOString()\n          }, null, 2)\n        }],\n      };\n    } catch (error: any) {\n      return {\n        content: [{ \n          type: \"text\" as const, \n          text: `Error checking auth status: ${error.message}` \n        }],\n        isError: true\n      };\n    }\n  }\n);\n\n// Add tool for requesting additional Graph permissions\nserver.tool(\n  \"add-graph-permission\",\n  \"Request additional Microsoft Graph permission scopes by performing a fresh interactive sign-in. This tool only works in interactive authentication mode and should be used if any Graph API call returns permissions related errors.\",\n  {\n    scopes: z.array(z.string()).describe(\"Array of Microsoft Graph permission scopes to request (e.g., ['User.Read', 'Mail.ReadWrite', 'Directory.Read.All'])\")\n  },\n  async ({ scopes }) => {\n    try {\n      // Check if we're in interactive mode\n      if (!authManager || authManager.getAuthMode() !== AuthMode.Interactive) {\n        const currentMode = authManager?.getAuthMode() || \"Not initialized\";\n        const clientId = process.env.CLIENT_ID;\n        \n        let errorMessage = `Error: add-graph-permission tool is only available in interactive authentication mode. Current mode: ${currentMode}.\\n\\n`;\n        \n        if (currentMode === AuthMode.ClientCredentials) {\n          errorMessage += `📋 To add permissions in Client Credentials mode:\\n`;\n          errorMessage += `1. Open the Microsoft Entra admin center (https://entra.microsoft.com)\\n`;\n          errorMessage += `2. Navigate to Applications > App registrations\\n`;\n          errorMessage += `3. Find your application${clientId ? ` (Client ID: ${clientId})` : ''}\\n`;\n          errorMessage += `4. Go to API permissions\\n`;\n          errorMessage += `5. Click \"Add a permission\" and select Microsoft Graph\\n`;\n          errorMessage += `6. Choose \"Application permissions\" and add the required scopes:\\n`;\n          errorMessage += `   ${scopes.map(scope => `• ${scope}`).join('\\n   ')}\\n`;\n          errorMessage += `7. Click \"Grant admin consent\" to approve the permissions\\n`;\n          errorMessage += `8. Restart the MCP server to use the new permissions`;\n        } else if (currentMode === AuthMode.ClientProvidedToken) {\n          errorMessage += `📋 To add permissions in Client Provided Token mode:\\n`;\n          errorMessage += `1. Obtain a new access token that includes the required scopes:\\n`;\n          errorMessage += `   ${scopes.map(scope => `• ${scope}`).join('\\n   ')}\\n`;\n          errorMessage += `2. When obtaining the token, ensure these scopes are included in the consent prompt\\n`;\n          errorMessage += `3. Use the set-access-token tool to update the server with the new token\\n`;\n          errorMessage += `4. The new token will include the additional permissions`;\n        } else {\n          errorMessage += `To use interactive permission requests, set USE_INTERACTIVE=true in environment variables and restart the server.`;\n        }\n        \n        return {\n          content: [{ \n            type: \"text\" as const, \n            text: errorMessage\n          }],\n          isError: true\n        };\n      }\n\n      // Validate scopes array\n      if (!scopes || scopes.length === 0) {\n        return {\n          content: [{ \n            type: \"text\" as const, \n            text: \"Error: At least one permission scope must be specified.\" \n          }],\n          isError: true\n        };\n      }\n\n      // Validate scope format (basic validation)\n      const invalidScopes = scopes.filter(scope => !scope.includes('.') || scope.trim() !== scope);\n      if (invalidScopes.length > 0) {\n        return {\n          content: [{ \n            type: \"text\" as const, \n            text: `Error: Invalid scope format detected: ${invalidScopes.join(', ')}. Scopes should be in format like 'User.Read' or 'Mail.ReadWrite'.` \n          }],\n          isError: true\n        };\n      }\n\n      logger.info(`Requesting additional Graph permissions: ${scopes.join(', ')}`);\n\n      // Get current configuration with defaults for interactive auth\n      const tenantId = process.env.TENANT_ID || LokkaDefaultTenantId;\n      const clientId = process.env.CLIENT_ID || LokkaClientId;\n      const redirectUri = process.env.REDIRECT_URI || LokkaDefaultRedirectUri;\n\n      logger.info(`Using tenant ID: ${tenantId}, client ID: ${clientId} for interactive authentication`);\n\n      // Create a new interactive credential with the requested scopes\n      const { InteractiveBrowserCredential, DeviceCodeCredential } = await import(\"@azure/identity\");\n      \n      // Clear any existing auth manager to force fresh authentication\n      authManager = null;\n      graphClient = null;\n      \n      // Request token with the new scopes - this will trigger interactive authentication\n      const scopeString = scopes.map(scope => `https://graph.microsoft.com/${scope}`).join(' ');\n      logger.info(`Requesting fresh token with scopes: ${scopeString}`);\n      \n      console.log(`\\n🔐 Requesting Additional Graph Permissions:`);\n      console.log(`Scopes: ${scopes.join(', ')}`);\n      console.log(`You will be prompted to sign in to grant these permissions.\\n`);\n\n      let newCredential;\n      let tokenResponse;\n      \n      try {\n        // Try Interactive Browser first - create fresh instance each time\n        newCredential = new InteractiveBrowserCredential({\n          tenantId: tenantId,\n          clientId: clientId,\n          redirectUri: redirectUri,\n        });\n        \n        // Request token immediately after creating credential\n        tokenResponse = await newCredential.getToken(scopeString);\n        \n      } catch (error) {\n        // Fallback to Device Code flow\n        logger.info(\"Interactive browser failed, falling back to device code flow\");\n        newCredential = new DeviceCodeCredential({\n          tenantId: tenantId,\n          clientId: clientId,\n          userPromptCallback: (info) => {\n            console.log(`\\n🔐 Additional Permissions Required:`);\n            console.log(`Please visit: ${info.verificationUri}`);\n            console.log(`And enter code: ${info.userCode}`);\n            console.log(`Requested scopes: ${scopes.join(', ')}\\n`);\n            return Promise.resolve();\n          },\n        });\n        \n        // Request token with device code credential\n        tokenResponse = await newCredential.getToken(scopeString);\n      }\n\n      if (!tokenResponse) {\n        return {\n          content: [{ \n            type: \"text\" as const, \n            text: \"Error: Failed to acquire access token with the requested scopes. Please check your permissions and try again.\" \n          }],\n          isError: true\n        };\n      }\n\n      // Create a completely new auth manager instance with the updated credential\n      const authConfig: AuthConfig = {\n        mode: AuthMode.Interactive,\n        tenantId,\n        clientId,\n        redirectUri\n      };\n\n      // Create a new auth manager instance\n      authManager = new AuthManager(authConfig);\n      \n      // Manually set the credential to our new one with the additional scopes\n      (authManager as any).credential = newCredential;\n\n      // DO NOT call initialize() as it might interfere with our fresh token\n      // Instead, directly create the Graph client with the new credential\n      const authProvider = authManager.getGraphAuthProvider();\n      graphClient = Client.initWithMiddleware({\n        authProvider: authProvider,\n      });\n\n      // Get the token status to show the new scopes\n      const tokenStatus = await authManager.getTokenStatus();\n\n      logger.info(`Successfully acquired fresh token with additional scopes: ${scopes.join(', ')}`);\n\n      return {\n        content: [{ \n          type: \"text\" as const, \n          text: JSON.stringify({\n            message: \"Successfully acquired additional Microsoft Graph permissions with fresh authentication\",\n            requestedScopes: scopes,\n            tokenStatus: tokenStatus,\n            note: \"A fresh sign-in was performed to ensure the new permissions are properly granted\",\n            timestamp: new Date().toISOString()\n          }, null, 2)\n        }],\n      };\n\n    } catch (error: any) {\n      logger.error(\"Error requesting additional Graph permissions:\", error);\n      return {\n        content: [{ \n          type: \"text\" as const, \n          text: `Error requesting additional permissions: ${error.message}` \n        }],\n        isError: true\n      };\n    }\n  }\n);\n\n// Start the server with stdio transport\nasync function main() {\n  // Determine authentication mode based on environment variables\n  const useCertificate = process.env.USE_CERTIFICATE === 'true';\n  const useInteractive = process.env.USE_INTERACTIVE === 'true';\n  const useClientToken = process.env.USE_CLIENT_TOKEN === 'true';\n  const initialAccessToken = process.env.ACCESS_TOKEN;\n  \n  let authMode: AuthMode;\n  \n  // Ensure only one authentication mode is enabled at a time\n  const enabledModes = [\n    useClientToken,\n    useInteractive,\n    useCertificate\n  ].filter(Boolean);\n\n  if (enabledModes.length > 1) {\n    throw new Error(\n      \"Multiple authentication modes enabled. Please enable only one of USE_CLIENT_TOKEN, USE_INTERACTIVE, or USE_CERTIFICATE.\"\n    );\n  }\n\n  if (useClientToken) {\n    authMode = AuthMode.ClientProvidedToken;\n    if (!initialAccessToken) {\n      logger.info(\"Client token mode enabled but no initial token provided. Token must be set via set-access-token tool.\");\n    }\n  } else if (useInteractive) {\n    authMode = AuthMode.Interactive;\n  } else if (useCertificate) {\n    authMode = AuthMode.Certificate;\n  } else {\n    // Check if we have client credentials environment variables\n    const hasClientCredentials = process.env.TENANT_ID && process.env.CLIENT_ID && process.env.CLIENT_SECRET;\n    \n    if (hasClientCredentials) {\n      authMode = AuthMode.ClientCredentials;\n    } else {\n      // Default to interactive mode for better user experience\n      authMode = AuthMode.Interactive;\n      logger.info(\"No authentication mode specified and no client credentials found. Defaulting to interactive mode.\");\n    }\n  }\n\n  logger.info(`Starting with authentication mode: ${authMode}`);\n\n  // Get tenant ID and client ID with defaults only for interactive mode\n  let tenantId: string | undefined;\n  let clientId: string | undefined;\n  \n  if (authMode === AuthMode.Interactive) {\n    // Interactive mode can use defaults\n    tenantId = process.env.TENANT_ID || LokkaDefaultTenantId;\n    clientId = process.env.CLIENT_ID || LokkaClientId;\n    logger.info(`Interactive mode using tenant ID: ${tenantId}, client ID: ${clientId}`);\n  } else {\n    // All other modes require explicit values from environment variables\n    tenantId = process.env.TENANT_ID;\n    clientId = process.env.CLIENT_ID;\n  }\n\n  const clientSecret = process.env.CLIENT_SECRET;\n  const certificatePath = process.env.CERTIFICATE_PATH;\n  const certificatePassword = process.env.CERTIFICATE_PASSWORD; // optional\n\n  // Validate required configuration\n  if (authMode === AuthMode.ClientCredentials) {\n    if (!tenantId || !clientId || !clientSecret) {\n      throw new Error(\"Client credentials mode requires explicit TENANT_ID, CLIENT_ID, and CLIENT_SECRET environment variables\");\n    }\n  } else if (authMode === AuthMode.Certificate) {\n    if (!tenantId || !clientId || !certificatePath) {\n      throw new Error(\"Certificate mode requires explicit TENANT_ID, CLIENT_ID, and CERTIFICATE_PATH environment variables\");\n    }\n  }\n  // Note: Client token mode can start without a token and receive it later\n\n  const authConfig: AuthConfig = {\n    mode: authMode,\n    tenantId,\n    clientId,\n    clientSecret,\n    accessToken: initialAccessToken,\n    redirectUri: process.env.REDIRECT_URI,\n    certificatePath,\n    certificatePassword\n  };\n\n  authManager = new AuthManager(authConfig);\n  \n  // Only initialize if we have required config (for client token mode, we can start without a token)\n  if (authMode !== AuthMode.ClientProvidedToken || initialAccessToken) {\n    await authManager.initialize();\n    \n    // Initialize Graph Client\n    const authProvider = authManager.getGraphAuthProvider();\n    graphClient = Client.initWithMiddleware({\n      authProvider: authProvider,\n    });\n    \n    logger.info(`Authentication initialized successfully using ${authMode} mode`);\n  } else {\n    logger.info(\"Started in client token mode. Use set-access-token tool to provide authentication token.\");\n  }\n\n  const transport = new StdioServerTransport();\n  await server.connect(transport);\n}\n\nmain().catch((error) => {\n  console.error(\"Fatal error in main():\", error);\n  logger.error(\"Fatal error in main()\", error);\n  process.exit(1);\n});\n"
  },
  {
    "path": "src/mcp/tsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n      \"target\": \"ES2022\",\n      \"module\": \"Node16\",\n      \"moduleResolution\": \"Node16\",\n      \"outDir\": \"./build\",\n      \"rootDir\": \"./src\",\n      \"strict\": true,\n      \"esModuleInterop\": true,\n      \"skipLibCheck\": true,\n      \"forceConsistentCasingInFileNames\": true\n    },\n    \"include\": [\"src/**/*\"],\n    \"exclude\": [\"node_modules\"]\n  }"
  },
  {
    "path": "website/.gitignore",
    "content": "# Dependencies\n/node_modules\n\n# Production\n/build\n\n# Generated files\n.docusaurus\n.cache-loader\n\n# Misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "website/README.md",
    "content": "# Website\n\nThis website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.\n\n### Installation\n\n```\n$ yarn\n```\n\n### Local Development\n\n```\n$ yarn start\n```\n\nThis command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.\n\n### Build\n\n```\n$ yarn build\n```\n\nThis command generates static content into the `build` directory and can be served using any static contents hosting service.\n\n### Deployment\n\nUsing SSH:\n\n```\n$ USE_SSH=true yarn deploy\n```\n\nNot using SSH:\n\n```\n$ GIT_USER=<Your GitHub username> yarn deploy\n```\n\nIf you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.\n"
  },
  {
    "path": "website/blog/2019-05-28-first-blog-post.md",
    "content": "---\nslug: first-blog-post\ntitle: First Blog Post\nauthors: [slorber, yangshun]\ntags: [hola, docusaurus]\n---\n\nLorem ipsum dolor sit amet...\n\n<!-- truncate -->\n\n...consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n"
  },
  {
    "path": "website/blog/2019-05-29-long-blog-post.md",
    "content": "---\nslug: long-blog-post\ntitle: Long Blog Post\nauthors: yangshun\ntags: [hello, docusaurus]\n---\n\nThis is the summary of a very long blog post,\n\nUse a `<!--` `truncate` `-->` comment to limit blog post size in the list view.\n\n<!-- truncate -->\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n"
  },
  {
    "path": "website/blog/2021-08-01-mdx-blog-post.mdx",
    "content": "---\nslug: mdx-blog-post\ntitle: MDX Blog Post\nauthors: [slorber]\ntags: [docusaurus]\n---\n\nBlog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/).\n\n:::tip\n\nUse the power of React to create interactive blog posts.\n\n:::\n\n{/* truncate */}\n\nFor example, use JSX to create an interactive button:\n\n```js\n<button onClick={() => alert('button clicked!')}>Click me!</button>\n```\n\n<button onClick={() => alert('button clicked!')}>Click me!</button>\n"
  },
  {
    "path": "website/blog/2021-08-26-welcome/index.md",
    "content": "---\nslug: welcome\ntitle: Welcome\nauthors: [slorber, yangshun]\ntags: [facebook, hello, docusaurus]\n---\n\n[Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog).\n\nHere are a few tips you might find useful.\n\n<!-- truncate -->\n\nSimply add Markdown files (or folders) to the `blog` directory.\n\nRegular blog authors can be added to `authors.yml`.\n\nThe blog post date can be extracted from filenames, such as:\n\n- `2019-05-30-welcome.md`\n- `2019-05-30-welcome/index.md`\n\nA blog post folder can be convenient to co-locate blog post images:\n\n![Docusaurus Plushie](./docusaurus-plushie-banner.jpeg)\n\nThe blog supports tags as well!\n\n**And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config.\n"
  },
  {
    "path": "website/blog/authors.yml",
    "content": "yangshun:\n  name: Yangshun Tay\n  title: Front End Engineer @ Facebook\n  url: https://github.com/yangshun\n  image_url: https://github.com/yangshun.png\n  page: true\n  socials:\n    x: yangshunz\n    github: yangshun\n\nslorber:\n  name: Sébastien Lorber\n  title: Docusaurus maintainer\n  url: https://sebastienlorber.com\n  image_url: https://github.com/slorber.png\n  page:\n    # customize the url of the author page at /blog/authors/<permalink>\n    permalink: '/all-sebastien-lorber-articles'\n  socials:\n    x: sebastienlorber\n    linkedin: sebastienlorber\n    github: slorber\n    newsletter: https://thisweekinreact.com\n"
  },
  {
    "path": "website/blog/tags.yml",
    "content": "facebook:\n  label: Facebook\n  permalink: /facebook\n  description: Facebook tag description\n\nhello:\n  label: Hello\n  permalink: /hello\n  description: Hello tag description\n\ndocusaurus:\n  label: Docusaurus\n  permalink: /docusaurus\n  description: Docusaurus tag description\n\nhola:\n  label: Hola\n  permalink: /hola\n  description: Hola tag description\n"
  },
  {
    "path": "website/docs/developer-guide.md",
    "content": "---\ntitle: 🧩 Developer guide\nsidebar_position: 4\n---\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\nFollow this guide if you want to build Lokka from source to contribute to the project.\n\n## Pre-requisites\n\n- Follow the [installation guide](install) to install Node and the [advanced guide](install-advanced) if you wish to create a custom Entra application.\n- Clone the Lokka repository from GitHub [https://github.com/merill/lokka](https://github.com/merill/lokka)\n\n## Building the project\n\n- Open a terminal and navigate to the Lokka project directory.\n- Change into the folder `\\src\\mcp\\`\n- Run the following command to install the dependencies:\n\n  ```bash\n  npm install\n  ```\n\n- After the dependencies are installed, run the following command to build the project:\n\n  ```bash\n  npm run build\n  ```\n- When the build is complete, you will see a main.js file find the compiled files in the `\\src\\mcp\\build\\` folder.\n\n## Configuring the agent\n  \n<Tabs>\n  <TabItem value=\"claude\" label=\"Claude\" default>\n\n- In Claude Desktop, open the settings by clicking on the hamburger icon in the top left corner.\n- Select **File** > **Settings** (or press `Ctrl + ,`)\n- In the **Developer** tab, click **Edit Config**\n- This opens explorer, edit `claude_desktop_config.json` in your favorite text editor.\n- Add the following configuration to the file, using the information you in the **Overview** blade of the Entra application you created earlier.\n\n- Note: On Windows the path needs to be escaped with `\\\\` or use `/` instead of `\\`.\n  - E.g. `C:\\\\Users\\\\<username>\\\\Documents\\\\lokka\\\\src\\\\mcp\\\\build\\\\main.js` or `C:/Users/<username>/Documents/lokka/src/mcp/build/main.js`\n- Tip: Right-click on `build\\main.js` in VS Code and select `Copy path` to copy the full path.\n\n```json\n{\n  \"mcpServers\": {\n      \"Lokka-Microsoft\": {\n          \"command\": \"node\",\n          \"args\": [\n              \"<absolute-path-to-main.js>/src/mcp/build/main.js\"\n          ],\n          \"env\": {\n            \"TENANT_ID\": \"<tenant-id>\",\n            \"CLIENT_ID\": \"<client-id>\",\n            \"CLIENT_SECRET\": \"<client-secret>\"\n          }\n      }\n  }\n}\n```\n\n- Exit Claude Desktop and restart it.\n  - Every time you make changes to the code or configuration, you need to restart Claude desktop for the changes to take effect.\n  - Note: In Windows, Claude doesn't exit when you close the window, it runs in the background. You can find it in the system tray. Right-click on the icon and select **Quit** to exit the application completely.\n\n### Testing the agent\n\n#### Testing with Claude Desktop\n\n- Open the Claude Desktop application.\n- In the chat window on the bottom right you should see a hammer icon if the configuration is correct.\n- Now you can start quering your Microsoft tenant using the Lokka agent tool.\n- Some sample queries you can try are:\n  - `Get all the users in my tenant`\n  - `Show me the details for John Doe`\n  - `Change John's department to IT` - Needs User.ReadWrite.All permission to be granted\n\n</TabItem>\n<TabItem value=\"vscode\" label=\"VS Code\">\n\n### Pre-requisites\n\n- Install the latest version of [VS Code - Insider](https://code.visualstudio.com/insiders/)\n- Install the latest version of [GitHub Copilot in VS Code](https://code.visualstudio.com/docs/copilot/setup)\n\n### VS Code\n\n- In VS Code, open the Command Palette by pressing `Ctrl + Shift +P` (or `Cmd + Shift + P` on Mac).\n- Type `MCP` and select `Command (stdio)`\n- Select\n  - Command: `node`\n  - Server ID: `Lokka-Microsoft`\n- Where to save configuration: `User Settings`\n- This will open the `settings.json` file in VS Code.\n\n- Add the following configuration to the file, using the information you in the **Overview** blade of the Entra application you created earlier.\n\n- Note: On Windows the path needs to be escaped with `\\\\` or use `/` instead of `\\`.\n  - E.g. `C:\\\\Users\\\\<username>\\\\Documents\\\\lokka\\\\src\\\\mcp\\\\build\\\\main.js` or `C:/Users/<username>/Documents/lokka/src/mcp/build/main.js`\n- Tip: Right-click on `build\\main.js` in VS Code and select `Copy path` to copy the full path.\n\n```json\n\"mcp\": {\n  \"servers\": {\n      \"Lokka-Microsoft\": {\n          \"command\": \"node\",\n          \"args\": [\n              \"<absolute-path-to-main.js>/src/mcp/build/main.js\"\n          ],\n          \"env\": {\n            \"TENANT_ID\": \"<tenant-id>\",\n            \"CLIENT_ID\": \"<client-id>\",\n            \"CLIENT_SECRET\": \"<client-secret>\"\n          }\n      }\n  }\n}\n```\n\n- `File` > `Save` to save the file.\n\n### Testing the agent\n\n- Start a new instance of VS Code (File > New Window)\n- Open `Copilot Edits` from `View` → `Copilot Edits`\n- At the bottom of the Copilot Edits panel (below the chat box)\n  - Select `Agent` (if it is showing `Edit`)\n  - Select `Claude 3.7 Sonnet` (if it is showing `GPT-40`)\n\n</TabItem>\n</Tabs>\n\n#### Testing with MCP Inspector\n\nMCP Inspector is a tool that allows you to test and debug your MCP server directly (without an LLM). It provides a user interface to send requests to the server and view the responses.\n\nSee the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) for more information.\n\n```console\nnpx @modelcontextprotocol/inspector node path/to/server/main.js args...\n```\n\n## Learn about MCP\n\n- [Model Context Protocol Tutorial by Matt Pocock](https://www.aihero.dev/model-context-protocol-tutorial) - This is a great tutorial that explains the Model Context Protocol and how to use it.\n- [Model Context Protocol docs](https://modelcontextprotocol.io/introduction) - The official docs for the Model Context Protocol.\n- [Model Context Protocol Clients](https://modelcontextprotocol.io/clients) - List of all the clients that support the Model Context Protocol.\n"
  },
  {
    "path": "website/docs/faq.md",
    "content": "---\nsidebar_position: 5\ntitle: 👨‍💻 FAQs\n---\n\n## Who built this?\n\nLokka is a personal project by Merill Fernando a Product Manager at Microsoft. To learn more about me and my other projects, visit my website at [merill.net](https://merill.net).\n\nI built this as a proof of concept to demonstrate the capabilities of using LLMs and MCPs for Microsoft 365 administration tasks.\n\nThis project is open source and available on [GitHub](https://github.com/merill/lokka).\n\n## What is the difference between Lokka and Copilot?\n\nCopilot is an enterprise grade AI solution from Microsoft and is natively integrated with Microsoft 365 while Lokka is an open source MCP server implementation for Microsoft Graph API.\n\nLokka is a simple middleware that allows you to use any compatible AI model and client.\n\nThis means you can experiment using paid offerings like Claude and Cursor or use open source models like Llama from Meta or Phi from Microsoft Research and run them completely offline on your own hardware.\n\n:::note\nLokka is not a replacement for Copilot and is not affiliated with Microsoft.\n:::\n\n## Can I use this in production?\n\nWe recommend using Lokka in a test environment for exploration and testing purposes. The aim of this project is to provide a playground to experiment with using LLMs for Microsoft 365 administration tasks.\n\n:::note\n\nLokka is not a production-ready solution and should not be used in a production environment. It is a proof of concept to demonstrate the capabilities of using LLMs for Microsoft 365 administration tasks.\n\n:::\n\n## Is this a Microsoft product?\n\nNo, Lokka is not a Microsoft product and is not affiliated with Microsoft.\n\n## How do I report issues?\n\nIf you encounter any issues or have suggestions for improvements, please open an issue on the [GitHub repository](https://github.com/merill/lokka/issues).\n\n## I'm seeing this error message, what should I do?\n\n### TypeError `[ERR_INVALID_ARG_TYPE]`: The \"path\" argument must be of type string. Received undefined\n\nMake sure you have the latest version of Node.js installed (v22.10.0 or higher). See [MCP Server issues](https://github.com/merill/lokka/issues/3) for other tips.\n"
  },
  {
    "path": "website/docs/install-advanced/app-only-auth.md",
    "content": "---\ntitle: 📦 App-only auth\nsidebar_position: 3\nslug: /install-advanced/app-only-auth\n---\n\nThis authentication method uses the client credentials flow to authenticate the agent with Microsoft Graph API.\n\nYou can use either certificate (recommended) or client secret authentication with the following configuration. In both instances, you need to create a Microsoft Entra application and grant it the necessary permissions.\n\n## Create an Entra app for App-Only auth with Lokka\n\n- Open [Entra admin center](https://entra.microsoft.com) > **Identity** > **Applications** > **App registrations**\n  - Tip: [enappreg.cmd.ms](https://enappreg.cmd.ms) is a shortcut to the App registrations page.\n- Select **New registration**\n- Enter a name for the application (e.g. `Lokka`)\n- Select **Register**\n- Select **API permissions** > **Add a permission**\n  - Select **Microsoft Graph** > **Application permissions**\n    - Search for each of the permissions and check the box next to each permission you want to allow.\n      - The agent will only be able to perform the actions based on the permissions you grant it.\n    - Select **Add permissions**\n- Select **Grant admin consent for [your organization]**\n- Select **Yes** to confirm\n\n## Option 1: App-Only Auth with Certificate (recommended for app-only auth)\n\nOnce the app is created and you've added a certificate you can configure the cert's location as shown below.\n\n```json\n{\n  \"Lokka-Microsoft\": {\n    \"command\": \"npx\",\n    \"args\": [\"-y\", \"@merill/lokka\"],\n    \"env\": {\n      \"TENANT_ID\": \"<tenant-id>\",\n      \"CLIENT_ID\": \"<client-id>\",\n      \"CERTIFICATE_PATH\": \"/path/to/certificate.pem\",\n      \"CERTIFICATE_PASSWORD\": \"<optional-certificate-password>\",\n      \"USE_CERTIFICATE\": \"true\"\n    }\n  }\n}\n```\n\nTip: Use the command below to convert a PFX client certificate to a PEM-encoded certificate.\n\n```bash\nopenssl pkcs12 -in /path/to/cert.pfx -out /path/to/cert.pem -nodes -clcerts\n```\n\n## Option 2: App-Only Auth with Client Secret\n\n### Create a client secret\n\n- In the Entra protal navigate to the app you created earlier\n- Select **Certificates & secrets** > **Client secrets** > **New client secret**\n- Enter a description for the secret (e.g. `Agent Config`)\n- Select **Add**\n- Copy the value of the secret, we will use this value in the agent configuration file.\n\nYou can now configure Lokka in VSCode, Claude using the config below.\n\n```json\n{\n  \"Lokka-Microsoft\": {\n    \"command\": \"npx\",\n    \"args\": [\"-y\", \"@merill/lokka\"],\n    \"env\": {\n      \"TENANT_ID\": \"<tenant-id>\",\n      \"CLIENT_ID\": \"<client-id>\",\n      \"CLIENT_SECRET\": \"<client-secret>\"\n    }\n  }\n}\n```\n"
  },
  {
    "path": "website/docs/install-advanced/interactive-auth.md",
    "content": "---\ntitle: 👤 Interactive auth\nsidebar_position: 2\nslug: /install-advanced/interactive-auth\n---\n\nThis authentication method opens a browser window and prompts the user to sign into their Microsoft tenant.\n\nIt currently requires the user to authenticate each time the client application (Claude, VS Code) is started.\n\nInteractive auth also allows the client to dynamically request and consent to additional permissions without having to look up the app in the Entra portal and grant permissions.\n\n## Option 1: Interactive auth with default app\n\nThis method is outlined in the quick start [Install Guide](/docs/install)\n\n## Option 2: Interactive auth with custom app\n\nIf you wish to use a custom Microsoft Entra app, you can create a new app registration in your Microsoft Entra tenant.\n\n### Create an Entra app for App-Only auth with Lokka \n\n- Open [Entra admin center](https://entra.microsoft.com) > **Identity** > **Applications** > **App registrations**\n  - Tip: [enappreg.cmd.ms](https://enappreg.cmd.ms) is a shortcut to the App registrations page.\n- Select **New registration**\n- Enter a name for the application (e.g. `Lokka`)\n- Leave the **Supported account types** as `Accounts in this organizational directory only (Single tenant)`.\n- In the **Redirect URI** section, select `Public client/native (mobile & desktop)` and enter `http://localhost`.\n- Select **Register**\n- Select **API permissions** > **Add a permission**\n  - Select **Microsoft Graph** > **Delegate permissions**\n    - Search for each of the permissions and check the box next to each permission you want to allow.\n    - Start with at least `User.Read.All` to be able to query users in your tenant (you can add more permissions later).\n      - The agent will only be able to perform the actions based on the permissions you grant it.\n    - Select **Add permissions**\n- Select **Grant admin consent for [your organization]**\n- Select **Yes** to confirm\n\nIn Claude desktop or VS Code you will need to provide the tenant ID and client ID of the application you just created.\n\nThe `USE_INTERACTIVE` needs to be set to `true` when using a custom app for interactive auth.\n\n```json\n{\n    \"Lokka-Microsoft\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@merill/lokka\"],\n      \"env\": {\n        \"TENANT_ID\": \"<tenant-id>\",\n        \"CLIENT_ID\": \"<client-id>\",\n        \"USE_INTERACTIVE\": \"true\"\n      }\n    }\n  }\n```"
  },
  {
    "path": "website/docs/install-advanced/readme.md",
    "content": "---\ntitle: 🛠️ Advanced install\nsidebar_position: 3\n---\n\n# Advanced Install Guide\n\nUse this guide if you want to configure Lokka with advanced options or use a custom Microsoft Entra application.\n\nThe quick start guide is sufficient for most users and you can find it [here](/docs/install). \n\nLokka supports multiple authentication options. Here's a quick summary of all the options.\n\n- 1️⃣ → [Interactive Auth](interactive-auth)\n  - Interactive auth with default app\n  - Interactive auth with custom app\n- 2️⃣ → [App-Only Auth](app-only-auth)\n  - App-Only Auth with Certificate\n  - App-Only Auth with Client Secret\n- 3️⃣ → [Token Auth](token-auth)\n"
  },
  {
    "path": "website/docs/install-advanced/token-auth.md",
    "content": "---\ntitle: 🔑 Token auth\nsidebar_position: 4\nslug: /install-advanced/token-auth\n---\n\nWith token auth, the user provides a valid Microsoft Graph access token to the Lokka agent. This method is useful in dev scenarios where you want to use an existing token from the Azure CLI or another tool like Graph Explorer.\n\nConfigure the Lokka agent to use token auth by setting the `USE_CLIENT_TOKEN` environment variable to `true`.\n\n```json\n{\n    \"Lokka-Microsoft\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@merill/lokka\"],\n      \"env\": {\n        \"USE_CLIENT_TOKEN\": \"true\"\n      }\n    }\n}\n```\n\nWhen using client-provided token mode:\n\n1. Start the MCP server with `USE_CLIENT_TOKEN=true`\n2. Use the `set-access-token` tool to provide a valid Microsoft Graph access token (Press # in chat and type the `set` to see the tools that start with `set-`)\n3. Use the `get-auth-status` tool to verify authentication status\n4. Refresh tokens as needed using `set-access-token`\n\n## Getting tokens\n\nYou can obtain a valid Microsoft Graph access token using the Azure CLI, Graph PowerShell or Graph Explorer.\n\nThis method is useful for development and testing purposes, but it is not recommended for production use due to security concerns.\n\nIn addition, access token are short-lived (typically 1 hour) and will need to be refreshed periodically.\n\n### Option 1: Graph Explorer\n\n1. Go to [Graph Explorer](https://aka.ms/ge)\n2. Sign in with your Microsoft account\n3. Select the **Access token** tab in the top pane below the URL bar\n\n#### To add additional permissions to the token\n\n1. Click on the **Modify permissions** button\n2. Search for the permissions you want to add (e.g. `User.Read.All`)\n3. Click **Add permissions**\n4. Click **Consent on behalf of your organization** to grant admin consent for the permissions\n5. Copy the access token from the **Access token** tab\n\n### Option 2: Azure CLI\n\n```bash\n# Login to Azure CLI\naz login\n\n# Get a token for Microsoft Graph\naz account get-access-token --resource https://graph.microsoft.com --query accessToken -o tsv\n```\n\n### Option 3: Graph PowerShell\n\n```powershell\n# Login to Graph PowerShell\nConnect-MgGraph\n\n# Get a token for Microsoft Graph\n$data = Invoke-MgGraphRequest -Uri \"https://graph.microsoft.com/v1.0/me\" -Method GET -OutputType HttpResponseMessage\n$data.RequestMessage.Headers.Authorization.Parameter\n\n```\n"
  },
  {
    "path": "website/docs/install.mdx",
    "content": "---\ntitle: 🚀 Install\nsidebar_position: 2\n---\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n# Install Lokka\n\nThis quick start guide will help you set up Lokka with the minimum configuration needed to get started.\n\nIf you want to learn more about the advanced configuration options, see the [Advanced Install Guide](/docs/install-advanced).\n\n## Pre-requisites\n\n- Install [Node.js](https://nodejs.org/en/download/)\n  - If you already have Node v22.10 or higher installed you can skip this step.\n  - Check by running `node -v` at the command prompt.\n\n## Configure the agent\n\nYou can use the Lokka agent tool with any compatible MCP client and LLM combo.\n\nClaude is the simplest way to get started and provides the best experience. You can use the free version of Claude Desktop to test Lokka (there are daily limits on the free version).\n\nGitHub Copilot Agent in VS Code is another great option.\n\n<Tabs>\n  <TabItem value=\"claude\" label=\"Claude\" default>\n\n### Install Claude Desktop\n\n- Download the latest version of Claude Desktop from [https://claude.ai/download](https://claude.ai/download)\n- Install the application by following the instructions on the website.\n- Open the application and sign in with your account (you can register for a free account).\n\n### Add Lokka to Claude Desktop\n\n- In Claude Desktop, open Settings by clicking on the hamburger icon in the top left corner.\n- Select **File** > **Settings** (or press `Ctrl + ,`)\n  - On Mac, you can find the settings in the top menu bar under **Claude** > **Settings** (or press `Cmd + ,`).\n- In the **Developer** tab, click **Edit Config**\n  - Note: If you don't see the Developer tab, you need to enable it first from `Help` > `Enable Developer Mode`.\n- This opens explorer, edit `claude_desktop_config.json` in your favorite text editor.\n- Add the following configuration to the file.\n\n```json\n{\n  \"mcpServers\": {\n    \"Lokka-Microsoft\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@merill/lokka\"]\n    }\n  }\n}\n```\n\n- Exit Claude Desktop and restart it.\n  - Every time you make changes to the code or configuration, you need to restart Claude desktop for the changes to take effect.\n  - Note: In Windows, Claude doesn't exit when you close the window, it runs in the background. You can find it in the system tray. Right-click on the icon and select **Quit** to exit the application completely.\n\n### Testing the agent\n\n- Open the Claude Desktop application.\n- You should see new browser window open and prompt you to sign into your Microsoft tenant.\n- Now you can start quering your Microsoft tenant using the Lokka agent tool.\n- Some sample queries you can try are:\n  - `Get all users in my tenant`\n  - `Show me the details for John Doe`\n  - `Change John's department to IT` - Needs User.ReadWrite.All permission to be granted\n  - `How many VMs do I have in my subscription?` - Needs Reader permission to be granted to the Azure subscription\n\n</TabItem>\n  <TabItem value=\"vscode\" label=\"VS Code\">\n### Pre-requisites\n\n- Install the latest version of [VS Code](https://code.visualstudio.com)\n\n### Add Lokka to GitHub Copilot\n\n<Tabs>\n  <TabItem value=\"oneClickInstall\" label=\"One Click Install\" default>\n  * Start **VS Code** and then click the button below to install Lokka in VS Code.\n  * If your browser prompts you to open VS Code, click **Open**.\n  * In the VS Code **Lokka-Microsoft** install page \n    * Click **Install**.\n    * Click the widget icon next to the button and select **Start Server**.\n  * This will open a browser window and prompt you to sign into your Microsoft tenant. \n\n  | Platform | VS Code | VS Code Insiders |\n  | - | - | - |\n  | Windows | [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Lokka_for_Windows-0098FF?style=flat-square&logo=visualstudiocode&logoColor=ffffff)](vscode:mcp/install?%7B%22name%22%3A%22Lokka-Microsoft%22%2C%22type%22%3A%22stdio%22%2C%22command%22%3A%22cmd%22%2C%22args%22%3A%5B%22%2Fc%22%2C%22npx%22%2C%22-y%22%2C%22%40merill%2Flokka%22%5D%7D) | [![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Lokka_for_Windows-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=ffffff)](vscode-insiders:mcp/install?%7B%22name%22%3A%22Lokka-Microsoft%22%2C%22type%22%3A%22stdio%22%2C%22command%22%3A%22cmd%22%2C%22args%22%3A%5B%22%2Fc%22%2C%22npx%22%2C%22-y%22%2C%22%40merill%2Flokka%22%5D%7D) |\n  | macOS/Linux | [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Lokka_for_macOS_%26_Linux-0098FF?style=flat-square&logo=visualstudiocode&logoColor=ffffff)](vscode:mcp/install?%7B%22name%22%3A%22Lokka-Microsoft%22%2C%22type%22%3A%22stdio%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40merill%2Flokka%22%5D%7D) | [![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Lokka_for_macOS_%26_Linux-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=ffffff)](vscode-insiders:mcp/install?%7B%22name%22%3A%22Lokka-Microsoft%22%2C%22type%22%3A%22stdio%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40merill%2Flokka%22%5D%7D) |\n\n:::note\n  If VS Code was not running when you clicked the button, you might need to click the button on this page again to install Lokka.\n:::\n\n  </TabItem>\n\n  <TabItem value=\"manualInstall\" label=\"Manual Install\" default>\n\n    - In VS Code, open the Command Palette by pressing `Ctrl + Shift +P` (or `Cmd + Shift + P` on Mac).\n    - Type `MCP: Add` and select `MCP: Add Server...`\n    - Select `Command (stdio)`\n    - Command:\n      - Windows: `cmd /c npx -y @merill/lokka`\n      - macOS/Linux: `npx -y @merill/lokka`\n    - Name: `Lokka-Microsoft`\n    - Where to install: `Global`\n    - This will open the `settings.json` file in VS Code.\n    - `File` > `Save` to save the file.\n    - Once you hit save, you should see a browser window open and prompt you to sign into your Microsoft tenant.\n  </TabItem>\n</Tabs>\n\n### Starting the MCP server manually\n\nTypically VSCode will automatically start the MCP server when needed, but you can also stop and start the MCP server manually.\n\n- Open the Command Palette again (`Ctrl + Shift +P`) and type `MCP` and select `MCP: List Servers`\n- Select `Lokka-Microsoft` from the list of servers.\n- Selet `Start Server`\n- This will start the Lokka server \n- Each time you hit Start you will see a browser window open and prompt you to sign into your Microsoft tenant.\n  - If you want to stay connected to the same tenant, you can use [AppApp-only authentication](/docs/install-advanced/app-only-auth).\n### Testing the agent\n\n- Start a new instance of VS Code (File > New Window)\n- Open `Chat` from `View` → `Chat`\n- At the bottom of the Chat panel (below the chat box)\n  - Select `Agent` (if it is showing `Ask` or `Edit`)\n  - Select `Claude Sonnet 4` or above (if it is showing `GPT-40`)\n\n- Now you can start querying your Microsoft tenant using the Lokka agent tool.\n- Some sample queries you can try are:\n  - `Get all users in my tenant`\n  - `Show me the details for John Doe`\n  - `Change John's department to IT` - Needs User.ReadWrite.All permission to be granted\n  - `How many VMs do I have in my subscription?` - Needs Reader permission to be granted to the Azure subscription\n\n:::note\n  If the chat prompts you to install VS GitHub Copilot for Azure, click the **rerun without** link to continue using Lokka.\n::: \n</TabItem>\n</Tabs>\n"
  },
  {
    "path": "website/docs/intro.md",
    "content": "---\nsidebar_position: 1\ntitle: 🤖 Introduction\n---\n\n## What is Lokka?\n\nLokka is a simple yet powerful middleware that connects AI language models (like ChatGPT or Claude) to your Azure and Microsoft 365 tenant using the Azure and Microsoft Graph APIs.\n\nThis allows you to perform administrative tasks using natural language queries.\n\n:::info\nIn technical terms, Lokka is an implementation of the [Model Context Protocol](https://modelcontextprotocol.io/introduction) (MCP) for the Microsoft Graph and Azure APIs.\n:::\n\nHere's a quick demo. Read on to learn how to set this up on your own machine.\n\n<img src=\"https://github.com/merill/lokka/blob/main/assets/lokka-demo-1.gif?raw=true\" alt=\"Lokka Demo - user create demo\" width=\"500\"/>\n\n### Sample queries\n\nHere are some examples of queries you can use with Lokka.\n\n- `Create a new security group called 'Sales and HR' with a dynamic rule based on the department attribute.`\n- `Find all the conditional access policies that haven't excluded the emergency access account`\n- `Show me all the Intune device configuration policies assigned to the 'Call center' group`\n- `What was the most expensive service in Azure last month?`\n\nYou can ask Lokka to do anything that Microsoft Graph can do which includes support for Entra, Intune, Teams and SharePoint. In addition to graph you can also work with your Azure resources as well.\n\n:::note\nThe agent will only be able to perform the actions based on the Graph and Azure permissions you grant it.\n:::\n\n## What is MCP?\n\n[Model Context Protocol](https://modelcontextprotocol.io/introduction) (MCP) is an open protocol that enables AI models to securely interact with local and remote resources through standardized server implementations.\n\nLokka is an implementation of the MCP protocol for the Microsoft Graph API.\n\n![How does Lokka work?](./assets/how-does-lokka-mcp-server-work.png)\n\n## Getting started\n\nWant to try Lokka? It's easy to get started!\n\nCheck out the [installation guide](./install).\n"
  },
  {
    "path": "website/docusaurus.config.js",
    "content": "// @ts-check\n// `@type` JSDoc annotations allow editor autocompletion and type checking\n// (when paired with `@ts-check`).\n// There are various equivalent ways to declare your Docusaurus config.\n// See: https://docusaurus.io/docs/api/docusaurus-config\n\nimport { themes as prismThemes } from \"prism-react-renderer\";\n\n// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)\n\n/** @type {import('@docusaurus/types').Config} */\nconst config = {\n  title: \"Lokka\",\n  tagline:\n    \"Beyond Commands, Beyond Clicks. A glimpse into the future of managing Microsoft 365 with AI!\",\n  favicon: \"img/favicon.ico\",\n\n  // Set the production url of your site here\n  url: \"https://lokka.dev\",\n  // Set the /<baseUrl>/ pathname under which your site is served\n  // For GitHub pages deployment, it is often '/<projectName>/'\n  baseUrl: \"/\",\n\n  // GitHub pages deployment config.\n  // If you aren't using GitHub pages, you don't need these.\n  organizationName: \"merill\", // Usually your GitHub org/user name.\n  projectName: \"lokka\", // Usually your repo name.\n\n  onBrokenLinks: \"ignore\",\n  onBrokenMarkdownLinks: \"warn\",\n\n  // Even if you don't use internationalization, you can use this field to set\n  // useful metadata like html lang. For example, if your site is Chinese, you\n  // may want to replace \"en\" with \"zh-Hans\".\n  i18n: {\n    defaultLocale: \"en\",\n    locales: [\"en\"],\n  },\n\n  presets: [\n    [\n      \"classic\",\n      /** @type {import('@docusaurus/preset-classic').Options} */\n      ({\n        docs: {\n          sidebarPath: \"./sidebars.js\",\n          // Please change this to your repo.\n          // Remove this to remove the \"edit this page\" links.\n          editUrl: \"https://github.com/merill/lokka/tree/main/website/\",\n        },\n        blog: {\n          showReadingTime: true,\n          feedOptions: {\n            type: [\"rss\", \"atom\"],\n            xslt: true,\n          },\n          // Please change this to your repo.\n          // Remove this to remove the \"edit this page\" links.\n          editUrl: \"https://github.com/merill/lokka/tree/main/\",\n          // Useful options to enforce blogging best practices\n          onInlineTags: \"warn\",\n          onInlineAuthors: \"warn\",\n          onUntruncatedBlogPosts: \"warn\",\n        },\n        theme: {\n          customCss: \"./src/css/custom.css\",\n        },\n      }),\n    ],\n  ],\n\n  themeConfig:\n    /** @type {import('@docusaurus/preset-classic').ThemeConfig} */\n    ({\n      // Replace with your project's social card\n      image: \"img/docusaurus-social-card.png\",\n      navbar: {\n        title: \"Lokka\",\n        logo: {\n          alt: \"Lokka logo\",\n          src: \"img/logo.svg\",\n        },\n        items: [\n          {\n            type: \"docSidebar\",\n            sidebarId: \"siteSidebar\",\n            position: \"left\",\n            label: \"Docs\",\n          },\n          {\n            href: \"https://merill.net\",\n            label: \"merill.net\",\n            position: \"right\",\n          },\n          {\n            \"aria-label\": \"GitHub Repository\",\n            className: \"navbar--github-link\",\n            href: \"https://github.com/merill/lokka\",\n            position: \"right\",\n          },\n        ],\n      },\n      footer: {\n        style: \"dark\",\n        links: [\n          {\n            title: \"My M365 tools\",\n            items: [\n              {\n                href: \"https://graphxray.merill.net\",\n                label: \"Graph X-Ray\",\n                position: \"right\",\n              },\n              {\n                href: \"https://graphpermissions.merill.net\",\n                label: \"Graph Permissions Explorer\",\n                position: \"right\",\n              },\n              {\n                href: \"https://maester.dev\",\n                label: \"Maester\",\n                position: \"right\",\n              },\n            ],\n          },\n          {\n            title: \"My other tools\",\n            items: [\n              {\n                href: \"https://cmd.ms\",\n                label: \"cmd.ms\",\n                position: \"right\",\n              },\n              {\n                href: \"https://akasearch.net\",\n                label: \"akasearch.net\",\n                position: \"right\",\n              },\n              {\n                href: \"https://mc.merill.net\",\n                label: \"Message Center Archive\",\n                position: \"right\",\n              },\n            ],\n          },\n          {\n            title: \"Follow Me\",\n            items: [\n              {\n                label: \"LinkedIn\",\n                href: \"https://linkedin.com/in/merill\",\n              },\n              {\n                label: \"Bluesky\",\n                href: \"https://bsky.app/profile/merill.net\",\n              },\n              {\n                label: \"X\",\n                href: \"https://x.com/merill\",\n              },\n            ],\n          },\n          {\n            title: \"My Entra specials\",\n            items: [\n              {\n                label: \"Entra.News - My weekly newsletter\",\n                href: \"https://entra.news\",\n              },\n              {\n                label: \"Entra.Chat - My weekly podcast\",\n                href: \"https://entra.chat\",\n              },\n              {\n                label: \"idPowerToys\",\n                href: \"https://idpowerapp.com\",\n              },\n            ],\n          },\n        ],\n        copyright: `Copyright © ${new Date().getFullYear()} Merill Fernando.`,\n      },\n      prism: {\n        theme: prismThemes.github,\n        darkTheme: prismThemes.dracula,\n      },\n    }),\n};\n\nexport default config;\n"
  },
  {
    "path": "website/package.json",
    "content": "{\n  \"name\": \"website\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"docusaurus\": \"docusaurus\",\n    \"start\": \"docusaurus start\",\n    \"build\": \"docusaurus build\",\n    \"swizzle\": \"docusaurus swizzle\",\n    \"deploy\": \"docusaurus deploy\",\n    \"clear\": \"docusaurus clear\",\n    \"serve\": \"docusaurus serve\",\n    \"write-translations\": \"docusaurus write-translations\",\n    \"write-heading-ids\": \"docusaurus write-heading-ids\"\n  },\n  \"dependencies\": {\n    \"@docusaurus/core\": \"3.7.0\",\n    \"@docusaurus/preset-classic\": \"3.7.0\",\n    \"@mdx-js/react\": \"^3.0.0\",\n    \"clsx\": \"^2.0.0\",\n    \"prism-react-renderer\": \"^2.3.0\",\n    \"react\": \"^19.0.0\",\n    \"react-dom\": \"^19.0.0\"\n  },\n  \"devDependencies\": {\n    \"@docusaurus/module-type-aliases\": \"3.7.0\",\n    \"@docusaurus/types\": \"3.7.0\"\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.5%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 3 chrome version\",\n      \"last 3 firefox version\",\n      \"last 5 safari version\"\n    ]\n  },\n  \"engines\": {\n    \"node\": \">=18.0\"\n  }\n}\n"
  },
  {
    "path": "website/sidebars.js",
    "content": "// @ts-check\n\n// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)\n\n/**\n * Creating a sidebar enables you to:\n - create an ordered group of docs\n - render a sidebar for each doc of that group\n - provide next/previous navigation\n\n The sidebars can be generated from the filesystem, or explicitly defined here.\n\n Create as many sidebars as you want.\n\n @type {import('@docusaurus/plugin-content-docs').SidebarsConfig}\n */\nconst sidebars = {\n  // By default, Docusaurus generates a sidebar from the docs folder structure\n  siteSidebar: [{type: 'autogenerated', dirName: '.'}],\n\n  // But you can create a sidebar manually\n  /*\n  tutorialSidebar: [\n    'intro',\n    'hello',\n    {\n      type: 'category',\n      label: 'Tutorial',\n      items: ['tutorial-basics/create-a-document'],\n    },\n  ],\n   */\n};\n\nexport default sidebars;\n"
  },
  {
    "path": "website/src/css/custom.css",
    "content": "/**\n * Any CSS included here will be global. The classic template\n * bundles Infima by default. Infima is a CSS framework designed to\n * work well for content-centric websites.\n */\n\n/* You can override the default Infima variables here. */\n:root {\n  --ifm-color-primary: #2e8555;\n  --ifm-color-primary-dark: #29784c;\n  --ifm-color-primary-darker: #277148;\n  --ifm-color-primary-darkest: #205d3b;\n  --ifm-color-primary-light: #33925d;\n  --ifm-color-primary-lighter: #359962;\n  --ifm-color-primary-lightest: #3cad6e;\n  --ifm-code-font-size: 95%;\n  --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);\n}\n\n/* For readability concerns, you should choose a lighter palette in dark mode. */\n[data-theme='dark'] {\n  --ifm-color-primary: #25c2a0;\n  --ifm-color-primary-dark: #21af90;\n  --ifm-color-primary-darker: #1fa588;\n  --ifm-color-primary-darkest: #1a8870;\n  --ifm-color-primary-light: #29d5b0;\n  --ifm-color-primary-lighter: #32d8b4;\n  --ifm-color-primary-lightest: #4fddbf;\n  --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);\n}\n\n.navbar--github-link {\n  width: 32px;\n  height: 32px;\n  padding: 6px;\n  margin-right: 20px;\n  margin-left: 6px;\n  border-radius: 50%;\n  transition: background var(--ifm-transition-fast);\n}\n\n.navbar--github-link:hover {\n  background: var(--ifm-color-emphasis-200);\n}\n\n.navbar--github-link:before {\n  content: \"\";\n  height: 100%;\n  display: block;\n  background: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E\")\n    no-repeat;\n}\n\nhtml[data-theme=\"dark\"] .navbar--github-link:before {\n  background: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='white' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E\")\n    no-repeat;\n}\n"
  },
  {
    "path": "website/src/pages/index.js",
    "content": "import useDocusaurusContext from \"@docusaurus/useDocusaurusContext\";\nimport Layout from \"@theme/Layout\";\nimport React, { useState, useRef, useEffect } from \"react\";\nimport Link from '@docusaurus/Link';\nimport styles from \"./styles.module.css\";\n\nfunction VideoPlayer() {\n  const [showVideo, setShowVideo] = useState(false);\n  const thumbnailRef = useRef(null);\n  const [tiltStyle, setTiltStyle] = useState({});\n  \n  const playVideo = () => {\n    setShowVideo(true);\n  };\n\n  useEffect(() => {\n    const container = thumbnailRef.current;\n    if (!container) return;\n\n    const handleMouseMove = (e) => {\n      if (showVideo) return;\n      \n      const rect = container.getBoundingClientRect();\n      const x = e.clientX - rect.left; // x position within the element\n      const y = e.clientY - rect.top;  // y position within the element\n      \n      // Calculate the tilt angle based on mouse position\n      // The further from center, the more tilt (up to max degrees)\n      const centerX = rect.width / 2;\n      const centerY = rect.height / 2;\n      \n      const maxTiltDegrees = 5; // Maximum tilt in degrees\n      const tiltX = ((y - centerY) / centerY) * -maxTiltDegrees;\n      const tiltY = ((x - centerX) / centerX) * maxTiltDegrees;\n      \n      setTiltStyle({\n        transform: `perspective(1000px) rotateX(${tiltX}deg) rotateY(${tiltY}deg)`,\n        transition: 'transform 0.05s ease-out'\n      });\n    };\n    \n    const handleMouseLeave = () => {\n      setTiltStyle({\n        transform: 'perspective(1000px) rotateX(0deg) rotateY(0deg)',\n        transition: 'transform 0.5s ease-out'\n      });\n    };\n\n    container.addEventListener('mousemove', handleMouseMove);\n    container.addEventListener('mouseleave', handleMouseLeave);\n\n    return () => {\n      container.removeEventListener('mousemove', handleMouseMove);\n      container.removeEventListener('mouseleave', handleMouseLeave);\n    };\n  }, [showVideo]);\n  \n  return (\n    <div className={styles.videoContainer}>\n      {!showVideo ? (\n        <div \n          ref={thumbnailRef}\n          className={styles.thumbnailContainer} \n          onClick={playVideo}\n          style={tiltStyle}\n        >\n          <img \n            className={styles.thumbnail} \n            src=\"/img/lokka-intro-video.png\" \n            alt=\"Lokka Demo - Introducing Lokka\" \n          />\n          <div className={styles.playButtonContainer}>\n            <div className={styles.playButtonOuter}>\n              <div className={styles.playButtonInner}>\n                <div className={styles.playIcon}></div>\n              </div>\n            </div>\n          </div>\n        </div>\n      ) : (\n        <iframe \n          className={styles.videoFrame}\n          src=\"https://www.youtube.com/embed/f-ECqQSpLCM?autoplay=1\"\n          title=\"Lokka Demo - Introducing Lokka\"\n          frameBorder=\"0\"\n          allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n          allowFullScreen\n        ></iframe>\n      )}\n    </div>\n  );\n}\n\nexport default function Home() {\n  const { siteConfig } = useDocusaurusContext();\n  return (\n    <Layout\n      title=\"Lokka\"\n      description=\"Lokka is an AI agent tool that brings the power of Microsoft Graph to AI agents like GitHub Copilot and Claude that run on your local desktop.\">\n      <main>\n        <div className={styles.hero}>\n          <div className={styles.container}>\n            <div className={styles.heroContent}>\n              <h1 className={styles.heroTitle}>Lokka</h1>\n              <p className={styles.heroSubtitle}>Lokka is an AI agent tool that brings the power of Microsoft Graph to AI agents like GitHub Copilot and Claude. The best part is you can get started for free and it runs on your desktop.</p>\n                <p className={styles.heroSubtitle}>Get a glimpse into the future of administering Microsoft 365 👇</p>\n            </div>\n            <VideoPlayer />\n            <div className={styles.buttonContainer}>\n              <Link\n                className={styles.tryButton}\n                to=\"/docs/install\">\n                Try Lokka\n              </Link>\n            </div>\n          </div>\n        </div>\n      </main>\n    </Layout>\n  );\n}\n"
  },
  {
    "path": "website/src/pages/index.module.css",
    "content": "/**\n * CSS files with the .module.css suffix will be treated as CSS modules\n * and scoped locally.\n */\n\n.heroBanner {\n  padding: 4rem 0;\n  text-align: center;\n  position: relative;\n  overflow: hidden;\n}\n\n@media screen and (max-width: 996px) {\n  .heroBanner {\n    padding: 2rem;\n  }\n}\n\n.buttons {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n"
  },
  {
    "path": "website/src/pages/styles.module.css",
    "content": ".hero {\n  padding: 4rem 0 6rem;\n  text-align: center;\n}\n\n.container {\n  max-width: 1200px;\n  margin: 0 auto;\n  padding: 0 1rem;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n\n.heroBanner {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  gap: 2rem;\n}\n\n.heroContent {\n  flex: 1;\n  margin-bottom: 2.5rem;\n  max-width: 800px;\n}\n\n.heroTitle {\n  font-size: 4rem;\n  margin-bottom: 1.5rem;\n  font-weight: 800;\n  letter-spacing: -0.05em;\n  line-height: 1.1;\n}\n\n.heroSubtitle {\n  font-size: 1.25rem;\n  margin-bottom: 2rem;\n  opacity: 0.8;\n  font-weight: 400;\n  line-height: 1.6;\n  letter-spacing: -0.015em;\n  max-width: 700px;\n  margin-left: auto;\n  margin-right: auto;\n}\n\n.videoContainer {\n  flex: 1;\n  width: 80%;\n  max-width: 900px;\n  border-radius: 16px;\n  overflow: hidden;\n  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);\n  position: relative;\n  margin: 0 auto 3rem;\n  transition: transform 0.3s ease;\n}\n\n.thumbnailContainer {\n  position: relative;\n  cursor: pointer;\n  width: 100%;\n  transform-style: preserve-3d;\n  transform: perspective(1000px);\n  will-change: transform;\n  transition: transform 0.3s ease;\n}\n\n.thumbnailContainer:hover {\n  transform: none;\n}\n\n.thumbnail {\n  width: 100%;\n  height: auto;\n  display: block;\n  border-radius: 16px;\n}\n\n.videoFrame {\n  width: 100%;\n  height: 500px;\n  display: block;\n  border-radius: 16px;\n}\n\n.playButtonContainer {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n}\n\n.playButtonOuter {\n  height: 90px;\n  width: 90px;\n  background: rgba(255, 255, 255, 0.8);\n  border-radius: 50%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  transition: transform 0.2s ease;\n}\n\n.playButtonInner {\n  height: 70px;\n  width: 70px;\n  background: black;\n  border-radius: 50%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.playIcon {\n  width: 0;\n  height: 0;\n  border-style: solid;\n  border-width: 12px 0 12px 24px;\n  border-color: transparent transparent transparent white;\n  margin-left: 5px;\n}\n\n.thumbnailContainer:hover .playButtonOuter {\n  transform: scale(1.1);\n}\n\n.buttonContainer {\n  margin-top: 1rem;\n  display: flex;\n  justify-content: center;\n}\n\n.tryButton {\n  display: inline-block;\n  background-color: #000;\n  color: #fff;\n  font-size: 1.1rem;\n  font-weight: 600;\n  padding: 14px 32px;\n  border-radius: 50px;\n  text-decoration: none;\n  transition: all 0.2s ease;\n  border: 2px solid #000;\n}\n\n.tryButton:hover {\n  background-color: #fff;\n  color: #000;\n  text-decoration: none;\n}\n\n/* Dark theme styles */\nhtml[data-theme='dark'] .tryButton {\n  background-color: #fff;\n  color: #000;\n  border: 2px solid #fff;\n}\n\nhtml[data-theme='dark'] .tryButton:hover {\n  background-color: transparent;\n  color: #fff;\n  border: 2px solid #fff;\n}\n\n@media screen and (max-width: 996px) {\n  .heroBanner {\n    flex-direction: column;\n    text-align: center;\n  }\n  \n  .heroTitle {\n    font-size: 3rem;\n  }\n  \n  .heroSubtitle {\n    font-size: 1.1rem;\n  }\n\n  .videoFrame {\n    height: 300px;\n  }\n  \n  .videoContainer {\n    width: 95%;\n  }\n  \n  .playButtonOuter {\n    height: 70px;\n    width: 70px;\n  }\n  \n  .playButtonInner {\n    height: 50px;\n    width: 50px;\n  }\n  \n  .playIcon {\n    border-width: 10px 0 10px 18px;\n  }\n}\n"
  },
  {
    "path": "website/static/.nojekyll",
    "content": ""
  }
]