This is page 1 of 2. Use http://codebase.md/merill/lokka?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .gitignore
├── assets
│   └── lokka-demo-1.gif
├── LICENSE
├── README.md
├── src
│   └── mcp
│       ├── build
│       │   ├── auth.js
│       │   ├── constants.js
│       │   ├── logger.js
│       │   └── main.js
│       ├── package-lock.json
│       ├── package.json
│       ├── README.md
│       ├── src
│       │   ├── auth.ts
│       │   ├── constants.ts
│       │   ├── logger.ts
│       │   └── main.ts
│       ├── TESTING.md
│       └── tsconfig.json
└── website
    ├── .gitignore
    ├── blog
    │   ├── 2019-05-28-first-blog-post.md
    │   ├── 2019-05-29-long-blog-post.md
    │   ├── 2021-08-01-mdx-blog-post.mdx
    │   ├── 2021-08-26-welcome
    │   │   ├── docusaurus-plushie-banner.jpeg
    │   │   └── index.md
    │   ├── authors.yml
    │   └── tags.yml
    ├── docs
    │   ├── assets
    │   │   └── how-does-lokka-mcp-server-work.png
    │   ├── developer-guide.md
    │   ├── faq.md
    │   ├── install-advanced
    │   │   ├── app-only-auth.md
    │   │   ├── interactive-auth.md
    │   │   ├── readme.md
    │   │   └── token-auth.md
    │   ├── install.mdx
    │   └── intro.md
    ├── docusaurus.config.js
    ├── package-lock.json
    ├── package.json
    ├── README.md
    ├── sidebars.js
    ├── src
    │   ├── css
    │   │   └── custom.css
    │   └── pages
    │       ├── index.js
    │       ├── index.module.css
    │       └── styles.module.css
    └── static
        ├── .nojekyll
        └── img
            ├── docusaurus-social-card.png
            ├── docusaurus.png
            ├── favicon.ico
            ├── logo.svg
            └── lokka-intro-video.png
```
# Files
--------------------------------------------------------------------------------
/website/static/.nojekyll:
--------------------------------------------------------------------------------
```
1 | 
```
--------------------------------------------------------------------------------
/website/.gitignore:
--------------------------------------------------------------------------------
```
 1 | # Dependencies
 2 | /node_modules
 3 | 
 4 | # Production
 5 | /build
 6 | 
 7 | # Generated files
 8 | .docusaurus
 9 | .cache-loader
10 | 
11 | # Misc
12 | .DS_Store
13 | .env.local
14 | .env.development.local
15 | .env.test.local
16 | .env.production.local
17 | 
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 | 
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
  1 | # Logs
  2 | logs
  3 | *.log
  4 | npm-debug.log*
  5 | yarn-debug.log*
  6 | yarn-error.log*
  7 | lerna-debug.log*
  8 | .pnpm-debug.log*
  9 | 
 10 | # Diagnostic reports (https://nodejs.org/api/report.html)
 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
 12 | 
 13 | # Runtime data
 14 | pids
 15 | *.pid
 16 | *.seed
 17 | *.pid.lock
 18 | 
 19 | # Directory for instrumented libs generated by jscoverage/JSCover
 20 | lib-cov
 21 | 
 22 | # Coverage directory used by tools like istanbul
 23 | coverage
 24 | *.lcov
 25 | 
 26 | # nyc test coverage
 27 | .nyc_output
 28 | 
 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
 30 | .grunt
 31 | 
 32 | # Bower dependency directory (https://bower.io/)
 33 | bower_components
 34 | 
 35 | # node-waf configuration
 36 | .lock-wscript
 37 | 
 38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
 39 | build/Release
 40 | 
 41 | # Dependency directories
 42 | node_modules/
 43 | jspm_packages/
 44 | 
 45 | # Snowpack dependency directory (https://snowpack.dev/)
 46 | web_modules/
 47 | 
 48 | # TypeScript cache
 49 | *.tsbuildinfo
 50 | 
 51 | # Optional npm cache directory
 52 | .npm
 53 | 
 54 | # Optional eslint cache
 55 | .eslintcache
 56 | 
 57 | # Optional stylelint cache
 58 | .stylelintcache
 59 | 
 60 | # Microbundle cache
 61 | .rpt2_cache/
 62 | .rts2_cache_cjs/
 63 | .rts2_cache_es/
 64 | .rts2_cache_umd/
 65 | 
 66 | # Optional REPL history
 67 | .node_repl_history
 68 | 
 69 | # Output of 'npm pack'
 70 | *.tgz
 71 | 
 72 | # Yarn Integrity file
 73 | .yarn-integrity
 74 | 
 75 | # dotenv environment variable files
 76 | .env
 77 | .env.development.local
 78 | .env.test.local
 79 | .env.production.local
 80 | .env.local
 81 | 
 82 | # parcel-bundler cache (https://parceljs.org/)
 83 | .cache
 84 | .parcel-cache
 85 | 
 86 | # Next.js build output
 87 | .next
 88 | out
 89 | 
 90 | # Nuxt.js build / generate output
 91 | .nuxt
 92 | dist
 93 | 
 94 | # Gatsby files
 95 | .cache/
 96 | # Comment in the public line in if your project uses Gatsby and not Next.js
 97 | # https://nextjs.org/blog/next-9-1#public-directory-support
 98 | # public
 99 | 
100 | # vuepress build output
101 | .vuepress/dist
102 | 
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 | 
107 | # vitepress build output
108 | **/.vitepress/dist
109 | 
110 | # vitepress cache directory
111 | **/.vitepress/cache
112 | 
113 | # Docusaurus cache and generated files
114 | .docusaurus
115 | 
116 | # Serverless directories
117 | .serverless/
118 | 
119 | # FuseBox cache
120 | .fusebox/
121 | 
122 | # DynamoDB Local files
123 | .dynamodb/
124 | 
125 | # TernJS port file
126 | .tern-port
127 | 
128 | # Stores VSCode versions used for testing VSCode extensions
129 | .vscode-test
130 | 
131 | # yarn v2
132 | .yarn/cache
133 | .yarn/unplugged
134 | .yarn/build-state.yml
135 | .yarn/install-state.gz
136 | .pnp.*
137 | src/.DS_Store
138 | .DS_Store
139 | website/static/img/Thumbs.db
140 | 
```
--------------------------------------------------------------------------------
/website/docs/install-advanced/readme.md:
--------------------------------------------------------------------------------
```markdown
 1 | ---
 2 | title: 🛠️ Advanced install
 3 | sidebar_position: 3
 4 | ---
 5 | 
 6 | # Advanced Install Guide
 7 | 
 8 | Use this guide if you want to configure Lokka with advanced options or use a custom Microsoft Entra application.
 9 | 
10 | The quick start guide is sufficient for most users and you can find it [here](/docs/install). 
11 | 
12 | Lokka supports multiple authentication options. Here's a quick summary of all the options.
13 | 
14 | - 1️⃣ → [Interactive Auth](interactive-auth)
15 |   - Interactive auth with default app
16 |   - Interactive auth with custom app
17 | - 2️⃣ → [App-Only Auth](app-only-auth)
18 |   - App-Only Auth with Certificate
19 |   - App-Only Auth with Client Secret
20 | - 3️⃣ → [Token Auth](token-auth)
21 | 
```
--------------------------------------------------------------------------------
/website/README.md:
--------------------------------------------------------------------------------
```markdown
 1 | # Website
 2 | 
 3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
 4 | 
 5 | ### Installation
 6 | 
 7 | ```
 8 | $ yarn
 9 | ```
10 | 
11 | ### Local Development
12 | 
13 | ```
14 | $ yarn start
15 | ```
16 | 
17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
18 | 
19 | ### Build
20 | 
21 | ```
22 | $ yarn build
23 | ```
24 | 
25 | This command generates static content into the `build` directory and can be served using any static contents hosting service.
26 | 
27 | ### Deployment
28 | 
29 | Using SSH:
30 | 
31 | ```
32 | $ USE_SSH=true yarn deploy
33 | ```
34 | 
35 | Not using SSH:
36 | 
37 | ```
38 | $ GIT_USER=<Your GitHub username> yarn deploy
39 | ```
40 | 
41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
42 | 
```
--------------------------------------------------------------------------------
/src/mcp/README.md:
--------------------------------------------------------------------------------
```markdown
  1 | # Lokka
  2 | 
  3 | [](https://badge.fury.io/js/@merill%2Flokka)
  4 | 
  5 | Lokka 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.
  6 | 
  7 | <img src="https://github.com/merill/lokka/blob/main/assets/lokka-demo-1.gif?raw=true" alt="Lokka Demo - user create demo" width="500"/>
  8 | 
  9 | Please see [Lokka.dev](https://lokka.dev) for how to use Lokka with your favorite AI model and chat client.
 10 | 
 11 | Lokka 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.
 12 | 
 13 | e.g.:
 14 | 
 15 | - `Create a new security group called 'Sales and HR' with a dynamic rule based on the department attribute.` 
 16 | - `Find all the conditional access policies that haven't excluded the emergency access account`
 17 | - `Show me all the Intune device configuration policies assigned to the 'Call center' group`
 18 | - `What was the most expensive service in Azure last month?`
 19 | 
 20 | 
 21 | 
 22 | ## Authentication Methods
 23 | 
 24 | Lokka now supports multiple authentication methods to accommodate different deployment scenarios:
 25 | 
 26 | ### Interactive Auth
 27 | 
 28 | For user-based authentication with interactive login, you can use the following configuration:
 29 | 
 30 | This is the simplest config and uses the default Lokka app.
 31 | 
 32 | ```json
 33 | {
 34 |   "mcpServers": {
 35 |     "Lokka-Microsoft": {
 36 |       "command": "npx",
 37 |       "args": ["-y", "@merill/lokka"]
 38 |     }
 39 |   }
 40 | }
 41 | ```
 42 | 
 43 | #### Interactive auth with custom app
 44 | 
 45 | If 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:
 46 | 
 47 | ```json
 48 | {
 49 |   "mcpServers": {
 50 |     "Lokka-Microsoft": {
 51 |       "command": "npx",
 52 |       "args": ["-y", "@merill/lokka"],
 53 |       "env": {
 54 |         "TENANT_ID": "<tenant-id>",
 55 |         "CLIENT_ID": "<client-id>",
 56 |         "USE_INTERACTIVE": "true"
 57 |       }
 58 |     }
 59 |   }
 60 | }
 61 | ```
 62 | 
 63 | ### App-Only Auth
 64 | 
 65 | Traditional app-only authentication. You can use either certificate (recommended) or client secret authentication with the following configuration.
 66 | 
 67 | See [Install Guide](https://lokka.dev/docs/install) for more details on how to create an Entra app.
 68 | 
 69 | #### App-Only Auth with Certificate
 70 | 
 71 | App only authentication using a PEM-encoded client certificate:
 72 | 
 73 | ```json
 74 | {
 75 |   "mcpServers": {
 76 |     "Lokka-Microsoft": {
 77 |       "command": "npx",
 78 |       "args": ["-y", "@merill/lokka"],
 79 |       "env": {
 80 |         "TENANT_ID": "<tenant-id>",
 81 |         "CLIENT_ID": "<client-id>",
 82 |         "CERTIFICATE_PATH": "/path/to/certificate.pem",
 83 |         "CERTIFICATE_PASSWORD": "<optional-certificate-password>",
 84 |         "USE_CERTIFICATE": "true"
 85 |       }
 86 |     }
 87 |   }
 88 | }
 89 | ```
 90 | 
 91 | For comfort, in order to convert a PFX client certificate to a PEM-encoded certificate:
 92 | 
 93 | ```bash
 94 | openssl pkcs12 -in /path/to/cert.pfx -out /path/to/cert.pem -nodes -clcerts
 95 | ```
 96 | 
 97 | #### #### App-Only Auth with Client Secret
 98 | 
 99 | ```json
100 | {
101 |   "mcpServers": {
102 |     "Lokka-Microsoft": {
103 |       "command": "npx",
104 |       "args": ["-y", "@merill/lokka"],
105 |       "env": {
106 |         "TENANT_ID": "<tenant-id>",
107 |         "CLIENT_ID": "<client-id>",
108 |         "CLIENT_SECRET": "<client-secret>"
109 |       }
110 |     }
111 |   }
112 | }
113 | ```
114 | 
115 | ### Client-Provided Token
116 | 
117 | Token-based authentication where the MCP Client provides access tokens:
118 | 
119 | ```json
120 | {
121 |   "mcpServers": {
122 |     "Lokka-Microsoft": {
123 |       "command": "npx",
124 |       "args": ["-y", "@merill/lokka"],
125 |       "env": {
126 |         "USE_CLIENT_TOKEN": "true"
127 |       }
128 |     }
129 |   }
130 | }
131 | ```
132 | 
133 | When using client-provided token mode:
134 | 
135 | 1. Start the MCP server with `USE_CLIENT_TOKEN=true`
136 | 2. Use the `set-access-token` tool to provide a valid Microsoft Graph access token
137 | 3. Use the `get-auth-status` tool to verify authentication status
138 | 4. Refresh tokens as needed using `set-access-token`
139 | 
140 | ## New Tools
141 | 
142 | ### Token Management Tools
143 | 
144 | - **`set-access-token`**: Set or update access tokens for Microsoft Graph authentication
145 | - **`get-auth-status`**: Check current authentication status and capabilities
146 | - **`add-graph-permission`**: Request additional Microsoft Graph permission scopes interactively
147 | 
148 | ### Graph API Version Control
149 | 
150 | Lokka now supports controlling the default Microsoft Graph API version used for all requests:
151 | 
152 | - **Default behavior**: Uses `beta` version for access to latest features
153 | - **Production mode**: Set `USE_GRAPH_BETA=false` to force all requests to use `v1.0` version
154 | - **Per-request override**: You can still specify `graphApiVersion` parameter in individual requests (unless `USE_GRAPH_BETA=false`)
155 | 
156 | When `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.
157 | 
158 | ## Getting started
159 | 
160 | See the docs for more information on how to install and configure Lokka.
161 | 
162 | - [Introduction](https://lokka.dev/)
163 | - [Install guide](https://lokka.dev/docs/install)
164 | - [Developer guide](https://lokka.dev/docs/developer-guide)
165 | 
166 | ## Components
167 | 
168 | ### Tools
169 | 
170 | 1. `Lokka-Microsoft`
171 |    - Call Microsoft Graph & Azure APIs. Supports querying Azure and Microsoft 365 tenants. Updates are also supported if permissions are provided.
172 |    - Input:
173 |      - `apiType` (string): Type of Microsoft API to query. Options: 'graph' for Microsoft Graph (Entra) or 'azure' for Azure Resource Management.
174 |      - `path` (string): The Azure or Graph API URL path to call (e.g. '/users', '/groups', '/subscriptions').
175 |      - `method` (string): HTTP method to use (e.g., get, post, put, patch, delete)
176 |      - `apiVersion` (string): Azure Resource Management API version (required for apiType Azure)
177 |      - `subscriptionId` (string): Azure Subscription ID (for Azure Resource Management).
178 |      - `queryParams` (string): Array of query parameters like $filter, $select, etc. All parameters are strings.
179 |      - `body` (JSON): The request body (for POST, PUT, PATCH)
180 |    - Returns: Results from the Azure or Graph API call.
181 | 
182 | 2. `set-access-token` *(New in v0.2.0)*
183 |    - Set or update an access token for Microsoft Graph authentication when using client-provided token mode.
184 |    - Input:
185 |      - `accessToken` (string): The access token obtained from Microsoft Graph authentication
186 |      - `expiresOn` (string, optional): Token expiration time in ISO format
187 |    - Returns: Confirmation of token update
188 | 
189 | 3. `get-auth-status` *(New in v0.2.0)*
190 |    - Check the current authentication status and mode of the MCP Server
191 |    - Returns: Authentication mode, readiness status, and capabilities
192 | 
193 | ### Environment Variables
194 | 
195 | The configuration of the server is done using environment variables. The following environment variables are supported:
196 | 
197 | | Name | Description | Required |
198 | |------|-------------|----------|
199 | | `TENANT_ID` | The ID of the Microsoft Entra tenant. | Yes (except for client-provided token mode) |
200 | | `CLIENT_ID` | The ID of the application registered in Microsoft Entra. | Yes (except for client-provided token mode) |
201 | | `CLIENT_SECRET` | The client secret of the application registered in Microsoft Entra. | Yes (for client credentials mode only) |
202 | | `USE_INTERACTIVE` | Set to "true" to enable interactive authentication mode. | No |
203 | | `USE_CLIENT_TOKEN` | Set to "true" to enable client-provided token authentication mode. | No |
204 | | `USE_CERTIFICATE` | Set to "true" to enable certificate authentication mode. | No |
205 | | `CERTIFICATE_PATH` | Path to the PEM-encoded certificate file for certificate authentication. | Yes (for certificate mode only) |
206 | | `CERTIFICATE_PASSWORD` | Password for the certificate file (if encrypted). | No |
207 | | `REDIRECT_URI` | Redirect URI for interactive authentication (default: `http://localhost:3200`). | No |
208 | | `ACCESS_TOKEN` | Initial access token for client-provided token mode. | No |
209 | | `USE_GRAPH_BETA` | Set to "false" to force all Graph API calls to use v1.0 instead of beta (default: true, allows beta). | No |
210 | 
211 | ## Contributors
212 | 
213 | - Interactive and Token-based Authentication (v0.2.0) - [@darrenjrobinson](https://github.com/darrenjrobinson)
214 | - Certificate Authentication (v0.2.1) - [@nitzpo](https://github.com/nitzpo)
215 | 
216 | ## Installation
217 | 
218 | To use this server with the Claude Desktop app, add the following configuration to the "mcpServers" section of your
219 | `claude_desktop_config.json`:
220 | 
221 | ### Interactive Authentication
222 | 
223 | ```json
224 | {
225 |   "mcpServers": {
226 |     "Lokka-Microsoft": {
227 |       "command": "npx",
228 |       "args": ["-y", "@merill/lokka"]
229 |     }
230 |   }
231 | }
232 | ```
233 | 
234 | ### Client Credentials Authentication
235 | 
236 | ```json
237 | {
238 |   "mcpServers": {
239 |     "Lokka-Microsoft": {
240 |       "command": "npx",
241 |       "args": ["-y", "@merill/lokka"],
242 |       "env": {
243 |         "TENANT_ID": "<tenant-id>",
244 |         "CLIENT_ID": "<client-id>",
245 |         "CLIENT_SECRET": "<client-secret>"
246 |       }
247 |     }
248 |   }
249 | }
250 | ```
251 | 
252 | Make 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.)
253 | 
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
  1 | # Lokka
  2 | 
  3 | [](https://badge.fury.io/js/@merill%2Flokka)
  4 | 
  5 | Lokka 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.
  6 | 
  7 | <img src="https://github.com/merill/lokka/blob/main/assets/lokka-demo-1.gif?raw=true" alt="Lokka Demo - user create demo" width="500"/>
  8 | 
  9 | Please see [Lokka.dev](https://lokka.dev) for how to use Lokka with your favorite AI model and chat client.
 10 | 
 11 | Lokka 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.
 12 | 
 13 | e.g.:
 14 | 
 15 | - `Create a new security group called 'Sales and HR' with a dynamic rule based on the department attribute.` 
 16 | - `Find all the conditional access policies that haven't excluded the emergency access account`
 17 | - `Show me all the Intune device configuration policies assigned to the 'Call center' group`
 18 | - `What was the most expensive service in Azure last month?`
 19 | 
 20 | 
 21 | 
 22 | ## Authentication Methods
 23 | 
 24 | Lokka now supports multiple authentication methods to accommodate different deployment scenarios:
 25 | 
 26 | ### Interactive Auth
 27 | 
 28 | For user-based authentication with interactive login, you can use the following configuration:
 29 | 
 30 | This is the simplest config and uses the default Lokka app.
 31 | 
 32 | ```json
 33 | {
 34 |   "mcpServers": {
 35 |     "Lokka-Microsoft": {
 36 |       "command": "npx",
 37 |       "args": ["-y", "@merill/lokka"]
 38 |     }
 39 |   }
 40 | }
 41 | ```
 42 | 
 43 | #### Interactive auth with custom app
 44 | 
 45 | If 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:
 46 | 
 47 | ```json
 48 | {
 49 |   "mcpServers": {
 50 |     "Lokka-Microsoft": {
 51 |       "command": "npx",
 52 |       "args": ["-y", "@merill/lokka"],
 53 |       "env": {
 54 |         "TENANT_ID": "<tenant-id>",
 55 |         "CLIENT_ID": "<client-id>",
 56 |         "USE_INTERACTIVE": "true"
 57 |       }
 58 |     }
 59 |   }
 60 | }
 61 | ```
 62 | 
 63 | ### App-Only Auth
 64 | 
 65 | Traditional app-only authentication. You can use either certificate (recommended) or client secret authentication with the following configuration.
 66 | 
 67 | See [Install Guide](https://lokka.dev/docs/install) for more details on how to create an Entra app.
 68 | 
 69 | #### App-Only Auth with Certificate
 70 | 
 71 | App only authentication using a PEM-encoded client certificate:
 72 | 
 73 | ```json
 74 | {
 75 |   "mcpServers": {
 76 |     "Lokka-Microsoft": {
 77 |       "command": "npx",
 78 |       "args": ["-y", "@merill/lokka"],
 79 |       "env": {
 80 |         "TENANT_ID": "<tenant-id>",
 81 |         "CLIENT_ID": "<client-id>",
 82 |         "CERTIFICATE_PATH": "/path/to/certificate.pem",
 83 |         "CERTIFICATE_PASSWORD": "<optional-certificate-password>",
 84 |         "USE_CERTIFICATE": "true"
 85 |       }
 86 |     }
 87 |   }
 88 | }
 89 | ```
 90 | 
 91 | For comfort, in order to convert a PFX client certificate to a PEM-encoded certificate:
 92 | 
 93 | ```bash
 94 | openssl pkcs12 -in /path/to/cert.pfx -out /path/to/cert.pem -nodes -clcerts
 95 | ```
 96 | 
 97 | #### App-Only Auth with Client Secret
 98 | 
 99 | ```json
100 | {
101 |   "mcpServers": {
102 |     "Lokka-Microsoft": {
103 |       "command": "npx",
104 |       "args": ["-y", "@merill/lokka"],
105 |       "env": {
106 |         "TENANT_ID": "<tenant-id>",
107 |         "CLIENT_ID": "<client-id>",
108 |         "CLIENT_SECRET": "<client-secret>"
109 |       }
110 |     }
111 |   }
112 | }
113 | ```
114 | 
115 | ### Client-Provided Token
116 | 
117 | Token-based authentication where the MCP Client provides access tokens:
118 | 
119 | ```json
120 | {
121 |   "mcpServers": {
122 |     "Lokka-Microsoft": {
123 |       "command": "npx",
124 |       "args": ["-y", "@merill/lokka"],
125 |       "env": {
126 |         "USE_CLIENT_TOKEN": "true"
127 |       }
128 |     }
129 |   }
130 | }
131 | ```
132 | 
133 | When using client-provided token mode:
134 | 
135 | 1. Start the MCP server with `USE_CLIENT_TOKEN=true`
136 | 2. Use the `set-access-token` tool to provide a valid Microsoft Graph access token
137 | 3. Use the `get-auth-status` tool to verify authentication status
138 | 4. Refresh tokens as needed using `set-access-token`
139 | 
140 | ## New Tools
141 | 
142 | ### Token Management Tools
143 | 
144 | - **`set-access-token`**: Set or update access tokens for Microsoft Graph authentication
145 | - **`get-auth-status`**: Check current authentication status and capabilities
146 | - **`add-graph-permission`**: Request additional Microsoft Graph permission scopes interactively
147 | 
148 | ### Graph API Version Control
149 | 
150 | Lokka now supports controlling the default Microsoft Graph API version used for all requests:
151 | 
152 | - **Default behavior**: Uses `beta` version for access to latest features
153 | - **Production mode**: Set `USE_GRAPH_BETA=false` to force all requests to use `v1.0` version
154 | - **Per-request override**: You can still specify `graphApiVersion` parameter in individual requests (unless `USE_GRAPH_BETA=false`)
155 | 
156 | When `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.
157 | 
158 | ## Getting started
159 | 
160 | See the docs for more information on how to install and configure Lokka.
161 | 
162 | - [Introduction](https://lokka.dev/)
163 | - [Install guide](https://lokka.dev/docs/install)
164 | - [Developer guide](https://lokka.dev/docs/developer-guide)
165 | 
166 | ### One-click install for VS Code
167 | 
168 |   | Platform | VS Code | VS Code Insiders |
169 |   | - | - | - |
170 |   | Windows | [](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) | [](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) |
171 |   | macOS/Linux | [](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) | [](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) |
172 | 
173 | 
174 | ## Components
175 | 
176 | ### Tools
177 | 
178 | 1. `Lokka-Microsoft`
179 |    - Call Microsoft Graph & Azure APIs. Supports querying Azure and Microsoft 365 tenants. Updates are also supported if permissions are provided.
180 |    - Input:
181 |      - `apiType` (string): Type of Microsoft API to query. Options: 'graph' for Microsoft Graph (Entra) or 'azure' for Azure Resource Management.
182 |      - `path` (string): The Azure or Graph API URL path to call (e.g. '/users', '/groups', '/subscriptions').
183 |      - `method` (string): HTTP method to use (e.g., get, post, put, patch, delete)
184 |      - `apiVersion` (string): Azure Resource Management API version (required for apiType Azure)
185 |      - `subscriptionId` (string): Azure Subscription ID (for Azure Resource Management).
186 |      - `queryParams` (string): Array of query parameters like $filter, $select, etc. All parameters are strings.
187 |      - `body` (JSON): The request body (for POST, PUT, PATCH)
188 |    - Returns: Results from the Azure or Graph API call.
189 | 
190 | 2. `set-access-token` *(New in v0.2.0)*
191 |    - Set or update an access token for Microsoft Graph authentication when using client-provided token mode.
192 |    - Input:
193 |      - `accessToken` (string): The access token obtained from Microsoft Graph authentication
194 |      - `expiresOn` (string, optional): Token expiration time in ISO format
195 |    - Returns: Confirmation of token update
196 | 
197 | 3. `get-auth-status` *(New in v0.2.0)*
198 |    - Check the current authentication status and mode of the MCP Server
199 |    - Returns: Authentication mode, readiness status, and capabilities
200 | 
201 | ### Environment Variables
202 | 
203 | The configuration of the server is done using environment variables. The following environment variables are supported:
204 | 
205 | | Name | Description | Required |
206 | |------|-------------|----------|
207 | | `TENANT_ID` | The ID of the Microsoft Entra tenant. | Yes (except for client-provided token mode) |
208 | | `CLIENT_ID` | The ID of the application registered in Microsoft Entra. | Yes (except for client-provided token mode) |
209 | | `CLIENT_SECRET` | The client secret of the application registered in Microsoft Entra. | Yes (for client credentials mode only) |
210 | | `USE_INTERACTIVE` | Set to "true" to enable interactive authentication mode. | No |
211 | | `USE_CLIENT_TOKEN` | Set to "true" to enable client-provided token authentication mode. | No |
212 | | `USE_CERTIFICATE` | Set to "true" to enable certificate authentication mode. | No |
213 | | `CERTIFICATE_PATH` | Path to the PEM-encoded certificate file for certificate authentication. | Yes (for certificate mode only) |
214 | | `CERTIFICATE_PASSWORD` | Password for the certificate file (if encrypted). | No |
215 | | `REDIRECT_URI` | Redirect URI for interactive authentication (default: `http://localhost:3000`). | No |
216 | | `ACCESS_TOKEN` | Initial access token for client-provided token mode. | No |
217 | | `USE_GRAPH_BETA` | Set to "false" to force all Graph API calls to use v1.0 instead of beta (default: true, allows beta). | No |
218 | 
219 | ## Contributors
220 | 
221 | - Interactive and Token-based Authentication (v0.2.0) - [@darrenjrobinson](https://github.com/darrenjrobinson)
222 | - Certificate Authentication (v0.2.1) - [@nitzpo](https://github.com/nitzpo)
223 | 
224 | ## Installation
225 | 
226 | To use this server with the Claude Desktop app, add the following configuration to the "mcpServers" section of your
227 | `claude_desktop_config.json`:
228 | 
229 | ### Interactive Authentication
230 | 
231 | ```json
232 | {
233 |   "mcpServers": {
234 |     "Lokka-Microsoft": {
235 |       "command": "npx",
236 |       "args": ["-y", "@merill/lokka"]
237 |     }
238 |   }
239 | }
240 | ```
241 | 
242 | ### Client Credentials Authentication
243 | 
244 | ```json
245 | {
246 |   "mcpServers": {
247 |     "Lokka-Microsoft": {
248 |       "command": "npx",
249 |       "args": ["-y", "@merill/lokka"],
250 |       "env": {
251 |         "TENANT_ID": "<tenant-id>",
252 |         "CLIENT_ID": "<client-id>",
253 |         "CLIENT_SECRET": "<client-secret>"
254 |       }
255 |     }
256 |   }
257 | }
258 | ```
259 | 
260 | Make 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.)
261 | 
```
--------------------------------------------------------------------------------
/website/blog/2019-05-28-first-blog-post.md:
--------------------------------------------------------------------------------
```markdown
 1 | ---
 2 | slug: first-blog-post
 3 | title: First Blog Post
 4 | authors: [slorber, yangshun]
 5 | tags: [hola, docusaurus]
 6 | ---
 7 | 
 8 | Lorem ipsum dolor sit amet...
 9 | 
10 | <!-- truncate -->
11 | 
12 | ...consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
13 | 
```
--------------------------------------------------------------------------------
/website/blog/tags.yml:
--------------------------------------------------------------------------------
```yaml
 1 | facebook:
 2 |   label: Facebook
 3 |   permalink: /facebook
 4 |   description: Facebook tag description
 5 | 
 6 | hello:
 7 |   label: Hello
 8 |   permalink: /hello
 9 |   description: Hello tag description
10 | 
11 | docusaurus:
12 |   label: Docusaurus
13 |   permalink: /docusaurus
14 |   description: Docusaurus tag description
15 | 
16 | hola:
17 |   label: Hola
18 |   permalink: /hola
19 |   description: Hola tag description
20 | 
```
--------------------------------------------------------------------------------
/website/src/pages/index.module.css:
--------------------------------------------------------------------------------
```css
 1 | /**
 2 |  * CSS files with the .module.css suffix will be treated as CSS modules
 3 |  * and scoped locally.
 4 |  */
 5 | 
 6 | .heroBanner {
 7 |   padding: 4rem 0;
 8 |   text-align: center;
 9 |   position: relative;
10 |   overflow: hidden;
11 | }
12 | 
13 | @media screen and (max-width: 996px) {
14 |   .heroBanner {
15 |     padding: 2rem;
16 |   }
17 | }
18 | 
19 | .buttons {
20 |   display: flex;
21 |   align-items: center;
22 |   justify-content: center;
23 | }
24 | 
```
--------------------------------------------------------------------------------
/src/mcp/tsconfig.json:
--------------------------------------------------------------------------------
```json
 1 | {
 2 |     "compilerOptions": {
 3 |       "target": "ES2022",
 4 |       "module": "Node16",
 5 |       "moduleResolution": "Node16",
 6 |       "outDir": "./build",
 7 |       "rootDir": "./src",
 8 |       "strict": true,
 9 |       "esModuleInterop": true,
10 |       "skipLibCheck": true,
11 |       "forceConsistentCasingInFileNames": true
12 |     },
13 |     "include": ["src/**/*"],
14 |     "exclude": ["node_modules"]
15 |   }
```
--------------------------------------------------------------------------------
/src/mcp/src/constants.ts:
--------------------------------------------------------------------------------
```typescript
 1 | // Shared constants for the Lokka MCP Server
 2 | 
 3 | export const LokkaClientId = "a9bac4c3-af0d-4292-9453-9da89e390140";
 4 | export const LokkaDefaultTenantId = "common";
 5 | export const LokkaDefaultRedirectUri = "http://localhost:3000";
 6 | 
 7 | // Default Graph API version based on USE_GRAPH_BETA environment variable
 8 | export const getDefaultGraphApiVersion = (): "v1.0" | "beta" => {
 9 |   return process.env.USE_GRAPH_BETA !== 'false' ? "beta" : "v1.0";
10 | };
11 | 
```
--------------------------------------------------------------------------------
/website/blog/2021-08-01-mdx-blog-post.mdx:
--------------------------------------------------------------------------------
```markdown
 1 | ---
 2 | slug: mdx-blog-post
 3 | title: MDX Blog Post
 4 | authors: [slorber]
 5 | tags: [docusaurus]
 6 | ---
 7 | 
 8 | Blog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/).
 9 | 
10 | :::tip
11 | 
12 | Use the power of React to create interactive blog posts.
13 | 
14 | :::
15 | 
16 | {/* truncate */}
17 | 
18 | For example, use JSX to create an interactive button:
19 | 
20 | ```js
21 | <button onClick={() => alert('button clicked!')}>Click me!</button>
22 | ```
23 | 
24 | <button onClick={() => alert('button clicked!')}>Click me!</button>
25 | 
```
--------------------------------------------------------------------------------
/website/blog/authors.yml:
--------------------------------------------------------------------------------
```yaml
 1 | yangshun:
 2 |   name: Yangshun Tay
 3 |   title: Front End Engineer @ Facebook
 4 |   url: https://github.com/yangshun
 5 |   image_url: https://github.com/yangshun.png
 6 |   page: true
 7 |   socials:
 8 |     x: yangshunz
 9 |     github: yangshun
10 | 
11 | slorber:
12 |   name: Sébastien Lorber
13 |   title: Docusaurus maintainer
14 |   url: https://sebastienlorber.com
15 |   image_url: https://github.com/slorber.png
16 |   page:
17 |     # customize the url of the author page at /blog/authors/<permalink>
18 |     permalink: '/all-sebastien-lorber-articles'
19 |   socials:
20 |     x: sebastienlorber
21 |     linkedin: sebastienlorber
22 |     github: slorber
23 |     newsletter: https://thisweekinreact.com
24 | 
```
--------------------------------------------------------------------------------
/website/blog/2021-08-26-welcome/index.md:
--------------------------------------------------------------------------------
```markdown
 1 | ---
 2 | slug: welcome
 3 | title: Welcome
 4 | authors: [slorber, yangshun]
 5 | tags: [facebook, hello, docusaurus]
 6 | ---
 7 | 
 8 | [Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog).
 9 | 
10 | Here are a few tips you might find useful.
11 | 
12 | <!-- truncate -->
13 | 
14 | Simply add Markdown files (or folders) to the `blog` directory.
15 | 
16 | Regular blog authors can be added to `authors.yml`.
17 | 
18 | The blog post date can be extracted from filenames, such as:
19 | 
20 | - `2019-05-30-welcome.md`
21 | - `2019-05-30-welcome/index.md`
22 | 
23 | A blog post folder can be convenient to co-locate blog post images:
24 | 
25 | 
26 | 
27 | The blog supports tags as well!
28 | 
29 | **And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config.
30 | 
```
--------------------------------------------------------------------------------
/website/sidebars.js:
--------------------------------------------------------------------------------
```javascript
 1 | // @ts-check
 2 | 
 3 | // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
 4 | 
 5 | /**
 6 |  * Creating a sidebar enables you to:
 7 |  - create an ordered group of docs
 8 |  - render a sidebar for each doc of that group
 9 |  - provide next/previous navigation
10 | 
11 |  The sidebars can be generated from the filesystem, or explicitly defined here.
12 | 
13 |  Create as many sidebars as you want.
14 | 
15 |  @type {import('@docusaurus/plugin-content-docs').SidebarsConfig}
16 |  */
17 | const sidebars = {
18 |   // By default, Docusaurus generates a sidebar from the docs folder structure
19 |   siteSidebar: [{type: 'autogenerated', dirName: '.'}],
20 | 
21 |   // But you can create a sidebar manually
22 |   /*
23 |   tutorialSidebar: [
24 |     'intro',
25 |     'hello',
26 |     {
27 |       type: 'category',
28 |       label: 'Tutorial',
29 |       items: ['tutorial-basics/create-a-document'],
30 |     },
31 |   ],
32 |    */
33 | };
34 | 
35 | export default sidebars;
36 | 
```
--------------------------------------------------------------------------------
/src/mcp/TESTING.md:
--------------------------------------------------------------------------------
```markdown
 1 | # How to Start Lokka MCP Server Locally and Test Microsoft Graph
 2 | 
 3 | This guide shows you how to start the Lokka MCP Server locally and test it with real Microsoft Graph API requests.
 4 | 
 5 | ## Prerequisites
 6 | 
 7 | 1. **Node.js** installed (v16 or later)
 8 | 2. **Valid Microsoft Graph access token** (see below for how to get one)
 9 | 3. **Build the project**: `npm run build`
10 | 
11 | ## Getting an Access Token
12 | 
13 | ### Option 1: Azure CLI (Easiest)
14 | 
15 | ```bash
16 | # Login to Azure CLI
17 | az login
18 | 
19 | # Get a token for Microsoft Graph
20 | az account get-access-token --resource https://graph.microsowft.com --query accessToken -o tsv
21 | ```
22 | 
23 | ### Option 2: Graph Explorer (Quick Testing)
24 | 
25 | 1. Go to https://developer.microsoft.com/en-us/graph/graph-explorer
26 | 2. Sign in with your Microsoft account
27 | 3. Open browser developer tools (F12)
28 | 4. Go to Network tab
29 | 5. Make any Graph request (like GET /me)
30 | 6. Find the request in Network tab
31 | 7. Copy the Authorization header value (remove "Bearer " prefix)
32 | 
```
--------------------------------------------------------------------------------
/src/mcp/src/logger.ts:
--------------------------------------------------------------------------------
```typescript
 1 | import { appendFileSync } from "fs";
 2 | import { join } from "path";
 3 | 
 4 | const LOG_FILE = join(
 5 |   import.meta.dirname,
 6 |   "mcp-server.log",
 7 | );
 8 | 
 9 | function formatMessage(
10 |   level: string,
11 |   message: string,
12 |   data?: unknown,
13 | ): string {
14 |   const timestamp = new Date().toISOString();
15 |   const dataStr = data
16 |     ? `\n${JSON.stringify(data, null, 2)}`
17 |     : "";
18 |   return `[${timestamp}] [${level}] ${message}${dataStr}\n`;
19 | }
20 | 
21 | export const logger = {
22 |   info(message: string, data?: unknown) {
23 |     const logMessage = formatMessage(
24 |       "INFO",
25 |       message,
26 |       data,
27 |     );
28 |     appendFileSync(LOG_FILE, logMessage);
29 |   },
30 | 
31 |   error(message: string, error?: unknown) {
32 |     const logMessage = formatMessage(
33 |       "ERROR",
34 |       message,
35 |       error,
36 |     );
37 |     appendFileSync(LOG_FILE, logMessage);
38 |   },
39 | 
40 |   // debug(message: string, data?: unknown) {
41 |   //   const logMessage = formatMessage(
42 |   //     "DEBUG",
43 |   //     message,
44 |   //     data,
45 |   //   );
46 |   //   appendFileSync(LOG_FILE, logMessage);
47 |   // },
48 | };
```
--------------------------------------------------------------------------------
/website/package.json:
--------------------------------------------------------------------------------
```json
 1 | {
 2 |   "name": "website",
 3 |   "version": "0.0.0",
 4 |   "private": true,
 5 |   "scripts": {
 6 |     "docusaurus": "docusaurus",
 7 |     "start": "docusaurus start",
 8 |     "build": "docusaurus build",
 9 |     "swizzle": "docusaurus swizzle",
10 |     "deploy": "docusaurus deploy",
11 |     "clear": "docusaurus clear",
12 |     "serve": "docusaurus serve",
13 |     "write-translations": "docusaurus write-translations",
14 |     "write-heading-ids": "docusaurus write-heading-ids"
15 |   },
16 |   "dependencies": {
17 |     "@docusaurus/core": "3.7.0",
18 |     "@docusaurus/preset-classic": "3.7.0",
19 |     "@mdx-js/react": "^3.0.0",
20 |     "clsx": "^2.0.0",
21 |     "prism-react-renderer": "^2.3.0",
22 |     "react": "^19.0.0",
23 |     "react-dom": "^19.0.0"
24 |   },
25 |   "devDependencies": {
26 |     "@docusaurus/module-type-aliases": "3.7.0",
27 |     "@docusaurus/types": "3.7.0"
28 |   },
29 |   "browserslist": {
30 |     "production": [
31 |       ">0.5%",
32 |       "not dead",
33 |       "not op_mini all"
34 |     ],
35 |     "development": [
36 |       "last 3 chrome version",
37 |       "last 3 firefox version",
38 |       "last 5 safari version"
39 |     ]
40 |   },
41 |   "engines": {
42 |     "node": ">=18.0"
43 |   }
44 | }
45 | 
```
--------------------------------------------------------------------------------
/src/mcp/package.json:
--------------------------------------------------------------------------------
```json
 1 | {
 2 |   "name": "@merill/lokka",
 3 |   "version": "0.3.0",
 4 |   "description": "Lokka is a Model Context Protocol (MCP) server for Microsoft Graph.",
 5 |   "license": "MIT",
 6 |   "author": "Merill",
 7 |   "homepage": "https://lokka.dev",
 8 |   "bugs": "https://github.com/merill/lokka/issues",
 9 |   "main": "main.js",
10 |   "type": "module",
11 |   "keywords": [
12 |     "mcp",
13 |     "graph",
14 |     "microsoft",
15 |     "graph",
16 |     "model",
17 |     "context",
18 |     "protocol"
19 |   ],
20 |   "repository": {
21 |     "type": "git",
22 |     "url": "git+https://github.com/merill/lokka.git",
23 |     "directory": "src/mcp"
24 |   },
25 |   "bin": {
26 |     "lokka": "build/main.js"
27 |   },
28 |   "scripts": {
29 |     "build": "tsc",
30 |     "build:unix": "tsc && chmod 755 build/main.js",
31 |     "demo:token": "node build/demo-token-auth.js",
32 |     "test:token": "node build/test-token-auth.js",
33 |     "test:simple": "node build/simple-token-test.js",
34 |     "test:live": "node build/live-test.js",
35 |     "start": "node build/main.js"
36 |   },
37 |   "files": [
38 |     "build"
39 |   ],
40 |   "dependencies": {
41 |     "@azure/identity": "^4.3.0",
42 |     "@microsoft/microsoft-graph-client": "^3.0.7",
43 |     "@modelcontextprotocol/sdk": "^1.7.0",
44 |     "@types/jsonwebtoken": "^9.0.10",
45 |     "isomorphic-fetch": "^3.0.0",
46 |     "jsonwebtoken": "^9.0.2",
47 |     "zod": "^3.24.2"
48 |   },
49 |   "devDependencies": {
50 |     "@types/isomorphic-fetch": "^0.0.39",
51 |     "@types/node": "^22.13.17",
52 |     "typescript": "^5.8.2"
53 |   }
54 | }
55 | 
```
--------------------------------------------------------------------------------
/website/docs/intro.md:
--------------------------------------------------------------------------------
```markdown
 1 | ---
 2 | sidebar_position: 1
 3 | title: 🤖 Introduction
 4 | ---
 5 | 
 6 | ## What is Lokka?
 7 | 
 8 | Lokka 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.
 9 | 
10 | This allows you to perform administrative tasks using natural language queries.
11 | 
12 | :::info
13 | In technical terms, Lokka is an implementation of the [Model Context Protocol](https://modelcontextprotocol.io/introduction) (MCP) for the Microsoft Graph and Azure APIs.
14 | :::
15 | 
16 | Here's a quick demo. Read on to learn how to set this up on your own machine.
17 | 
18 | <img src="https://github.com/merill/lokka/blob/main/assets/lokka-demo-1.gif?raw=true" alt="Lokka Demo - user create demo" width="500"/>
19 | 
20 | ### Sample queries
21 | 
22 | Here are some examples of queries you can use with Lokka.
23 | 
24 | - `Create a new security group called 'Sales and HR' with a dynamic rule based on the department attribute.`
25 | - `Find all the conditional access policies that haven't excluded the emergency access account`
26 | - `Show me all the Intune device configuration policies assigned to the 'Call center' group`
27 | - `What was the most expensive service in Azure last month?`
28 | 
29 | You 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.
30 | 
31 | :::note
32 | The agent will only be able to perform the actions based on the Graph and Azure permissions you grant it.
33 | :::
34 | 
35 | ## What is MCP?
36 | 
37 | [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.
38 | 
39 | Lokka is an implementation of the MCP protocol for the Microsoft Graph API.
40 | 
41 | 
42 | 
43 | ## Getting started
44 | 
45 | Want to try Lokka? It's easy to get started!
46 | 
47 | Check out the [installation guide](./install).
48 | 
```
--------------------------------------------------------------------------------
/website/docs/faq.md:
--------------------------------------------------------------------------------
```markdown
 1 | ---
 2 | sidebar_position: 5
 3 | title: 👨💻 FAQs
 4 | ---
 5 | 
 6 | ## Who built this?
 7 | 
 8 | Lokka 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).
 9 | 
10 | I built this as a proof of concept to demonstrate the capabilities of using LLMs and MCPs for Microsoft 365 administration tasks.
11 | 
12 | This project is open source and available on [GitHub](https://github.com/merill/lokka).
13 | 
14 | ## What is the difference between Lokka and Copilot?
15 | 
16 | Copilot 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.
17 | 
18 | Lokka is a simple middleware that allows you to use any compatible AI model and client.
19 | 
20 | This 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.
21 | 
22 | :::note
23 | Lokka is not a replacement for Copilot and is not affiliated with Microsoft.
24 | :::
25 | 
26 | ## Can I use this in production?
27 | 
28 | We 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.
29 | 
30 | :::note
31 | 
32 | Lokka 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.
33 | 
34 | :::
35 | 
36 | ## Is this a Microsoft product?
37 | 
38 | No, Lokka is not a Microsoft product and is not affiliated with Microsoft.
39 | 
40 | ## How do I report issues?
41 | 
42 | If you encounter any issues or have suggestions for improvements, please open an issue on the [GitHub repository](https://github.com/merill/lokka/issues).
43 | 
44 | ## I'm seeing this error message, what should I do?
45 | 
46 | ### TypeError `[ERR_INVALID_ARG_TYPE]`: The "path" argument must be of type string. Received undefined
47 | 
48 | Make 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.
49 | 
```
--------------------------------------------------------------------------------
/website/docs/install-advanced/token-auth.md:
--------------------------------------------------------------------------------
```markdown
 1 | ---
 2 | title: 🔑 Token auth
 3 | sidebar_position: 4
 4 | slug: /install-advanced/token-auth
 5 | ---
 6 | 
 7 | With 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.
 8 | 
 9 | Configure the Lokka agent to use token auth by setting the `USE_CLIENT_TOKEN` environment variable to `true`.
10 | 
11 | ```json
12 | {
13 |     "Lokka-Microsoft": {
14 |       "command": "npx",
15 |       "args": ["-y", "@merill/lokka"],
16 |       "env": {
17 |         "USE_CLIENT_TOKEN": "true"
18 |       }
19 |     }
20 | }
21 | ```
22 | 
23 | When using client-provided token mode:
24 | 
25 | 1. Start the MCP server with `USE_CLIENT_TOKEN=true`
26 | 2. 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-`)
27 | 3. Use the `get-auth-status` tool to verify authentication status
28 | 4. Refresh tokens as needed using `set-access-token`
29 | 
30 | ## Getting tokens
31 | 
32 | You can obtain a valid Microsoft Graph access token using the Azure CLI, Graph PowerShell or Graph Explorer.
33 | 
34 | This method is useful for development and testing purposes, but it is not recommended for production use due to security concerns.
35 | 
36 | In addition, access token are short-lived (typically 1 hour) and will need to be refreshed periodically.
37 | 
38 | ### Option 1: Graph Explorer
39 | 
40 | 1. Go to [Graph Explorer](https://aka.ms/ge)
41 | 2. Sign in with your Microsoft account
42 | 3. Select the **Access token** tab in the top pane below the URL bar
43 | 
44 | #### To add additional permissions to the token
45 | 
46 | 1. Click on the **Modify permissions** button
47 | 2. Search for the permissions you want to add (e.g. `User.Read.All`)
48 | 3. Click **Add permissions**
49 | 4. Click **Consent on behalf of your organization** to grant admin consent for the permissions
50 | 5. Copy the access token from the **Access token** tab
51 | 
52 | ### Option 2: Azure CLI
53 | 
54 | ```bash
55 | # Login to Azure CLI
56 | az login
57 | 
58 | # Get a token for Microsoft Graph
59 | az account get-access-token --resource https://graph.microsoft.com --query accessToken -o tsv
60 | ```
61 | 
62 | ### Option 3: Graph PowerShell
63 | 
64 | ```powershell
65 | # Login to Graph PowerShell
66 | Connect-MgGraph
67 | 
68 | # Get a token for Microsoft Graph
69 | $data = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/me" -Method GET -OutputType HttpResponseMessage
70 | $data.RequestMessage.Headers.Authorization.Parameter
71 | 
72 | ```
73 | 
```
--------------------------------------------------------------------------------
/website/docs/install-advanced/interactive-auth.md:
--------------------------------------------------------------------------------
```markdown
 1 | ---
 2 | title: 👤 Interactive auth
 3 | sidebar_position: 2
 4 | slug: /install-advanced/interactive-auth
 5 | ---
 6 | 
 7 | This authentication method opens a browser window and prompts the user to sign into their Microsoft tenant.
 8 | 
 9 | It currently requires the user to authenticate each time the client application (Claude, VS Code) is started.
10 | 
11 | Interactive 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.
12 | 
13 | ## Option 1: Interactive auth with default app
14 | 
15 | This method is outlined in the quick start [Install Guide](/docs/install)
16 | 
17 | ## Option 2: Interactive auth with custom app
18 | 
19 | If you wish to use a custom Microsoft Entra app, you can create a new app registration in your Microsoft Entra tenant.
20 | 
21 | ### Create an Entra app for App-Only auth with Lokka 
22 | 
23 | - Open [Entra admin center](https://entra.microsoft.com) > **Identity** > **Applications** > **App registrations**
24 |   - Tip: [enappreg.cmd.ms](https://enappreg.cmd.ms) is a shortcut to the App registrations page.
25 | - Select **New registration**
26 | - Enter a name for the application (e.g. `Lokka`)
27 | - Leave the **Supported account types** as `Accounts in this organizational directory only (Single tenant)`.
28 | - In the **Redirect URI** section, select `Public client/native (mobile & desktop)` and enter `http://localhost`.
29 | - Select **Register**
30 | - Select **API permissions** > **Add a permission**
31 |   - Select **Microsoft Graph** > **Delegate permissions**
32 |     - Search for each of the permissions and check the box next to each permission you want to allow.
33 |     - Start with at least `User.Read.All` to be able to query users in your tenant (you can add more permissions later).
34 |       - The agent will only be able to perform the actions based on the permissions you grant it.
35 |     - Select **Add permissions**
36 | - Select **Grant admin consent for [your organization]**
37 | - Select **Yes** to confirm
38 | 
39 | In Claude desktop or VS Code you will need to provide the tenant ID and client ID of the application you just created.
40 | 
41 | The `USE_INTERACTIVE` needs to be set to `true` when using a custom app for interactive auth.
42 | 
43 | ```json
44 | {
45 |     "Lokka-Microsoft": {
46 |       "command": "npx",
47 |       "args": ["-y", "@merill/lokka"],
48 |       "env": {
49 |         "TENANT_ID": "<tenant-id>",
50 |         "CLIENT_ID": "<client-id>",
51 |         "USE_INTERACTIVE": "true"
52 |       }
53 |     }
54 |   }
55 | ```
```
--------------------------------------------------------------------------------
/website/docs/install-advanced/app-only-auth.md:
--------------------------------------------------------------------------------
```markdown
 1 | ---
 2 | title: 📦 App-only auth
 3 | sidebar_position: 3
 4 | slug: /install-advanced/app-only-auth
 5 | ---
 6 | 
 7 | This authentication method uses the client credentials flow to authenticate the agent with Microsoft Graph API.
 8 | 
 9 | You 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.
10 | 
11 | ## Create an Entra app for App-Only auth with Lokka
12 | 
13 | - Open [Entra admin center](https://entra.microsoft.com) > **Identity** > **Applications** > **App registrations**
14 |   - Tip: [enappreg.cmd.ms](https://enappreg.cmd.ms) is a shortcut to the App registrations page.
15 | - Select **New registration**
16 | - Enter a name for the application (e.g. `Lokka`)
17 | - Select **Register**
18 | - Select **API permissions** > **Add a permission**
19 |   - Select **Microsoft Graph** > **Application permissions**
20 |     - Search for each of the permissions and check the box next to each permission you want to allow.
21 |       - The agent will only be able to perform the actions based on the permissions you grant it.
22 |     - Select **Add permissions**
23 | - Select **Grant admin consent for [your organization]**
24 | - Select **Yes** to confirm
25 | 
26 | ## Option 1: App-Only Auth with Certificate (recommended for app-only auth)
27 | 
28 | Once the app is created and you've added a certificate you can configure the cert's location as shown below.
29 | 
30 | ```json
31 | {
32 |   "Lokka-Microsoft": {
33 |     "command": "npx",
34 |     "args": ["-y", "@merill/lokka"],
35 |     "env": {
36 |       "TENANT_ID": "<tenant-id>",
37 |       "CLIENT_ID": "<client-id>",
38 |       "CERTIFICATE_PATH": "/path/to/certificate.pem",
39 |       "CERTIFICATE_PASSWORD": "<optional-certificate-password>",
40 |       "USE_CERTIFICATE": "true"
41 |     }
42 |   }
43 | }
44 | ```
45 | 
46 | Tip: Use the command below to convert a PFX client certificate to a PEM-encoded certificate.
47 | 
48 | ```bash
49 | openssl pkcs12 -in /path/to/cert.pfx -out /path/to/cert.pem -nodes -clcerts
50 | ```
51 | 
52 | ## Option 2: App-Only Auth with Client Secret
53 | 
54 | ### Create a client secret
55 | 
56 | - In the Entra protal navigate to the app you created earlier
57 | - Select **Certificates & secrets** > **Client secrets** > **New client secret**
58 | - Enter a description for the secret (e.g. `Agent Config`)
59 | - Select **Add**
60 | - Copy the value of the secret, we will use this value in the agent configuration file.
61 | 
62 | You can now configure Lokka in VSCode, Claude using the config below.
63 | 
64 | ```json
65 | {
66 |   "Lokka-Microsoft": {
67 |     "command": "npx",
68 |     "args": ["-y", "@merill/lokka"],
69 |     "env": {
70 |       "TENANT_ID": "<tenant-id>",
71 |       "CLIENT_ID": "<client-id>",
72 |       "CLIENT_SECRET": "<client-secret>"
73 |     }
74 |   }
75 | }
76 | ```
77 | 
```
--------------------------------------------------------------------------------
/website/blog/2019-05-29-long-blog-post.md:
--------------------------------------------------------------------------------
```markdown
 1 | ---
 2 | slug: long-blog-post
 3 | title: Long Blog Post
 4 | authors: yangshun
 5 | tags: [hello, docusaurus]
 6 | ---
 7 | 
 8 | This is the summary of a very long blog post,
 9 | 
10 | Use a `<!--` `truncate` `-->` comment to limit blog post size in the list view.
11 | 
12 | <!-- truncate -->
13 | 
14 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
15 | 
16 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
17 | 
18 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
19 | 
20 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
21 | 
22 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
23 | 
24 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
25 | 
26 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
27 | 
28 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
29 | 
30 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
31 | 
32 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
33 | 
34 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
35 | 
36 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
37 | 
38 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
39 | 
40 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
41 | 
42 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
43 | 
44 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
45 | 
```
--------------------------------------------------------------------------------
/website/src/css/custom.css:
--------------------------------------------------------------------------------
```css
 1 | /**
 2 |  * Any CSS included here will be global. The classic template
 3 |  * bundles Infima by default. Infima is a CSS framework designed to
 4 |  * work well for content-centric websites.
 5 |  */
 6 | 
 7 | /* You can override the default Infima variables here. */
 8 | :root {
 9 |   --ifm-color-primary: #2e8555;
10 |   --ifm-color-primary-dark: #29784c;
11 |   --ifm-color-primary-darker: #277148;
12 |   --ifm-color-primary-darkest: #205d3b;
13 |   --ifm-color-primary-light: #33925d;
14 |   --ifm-color-primary-lighter: #359962;
15 |   --ifm-color-primary-lightest: #3cad6e;
16 |   --ifm-code-font-size: 95%;
17 |   --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
18 | }
19 | 
20 | /* For readability concerns, you should choose a lighter palette in dark mode. */
21 | [data-theme='dark'] {
22 |   --ifm-color-primary: #25c2a0;
23 |   --ifm-color-primary-dark: #21af90;
24 |   --ifm-color-primary-darker: #1fa588;
25 |   --ifm-color-primary-darkest: #1a8870;
26 |   --ifm-color-primary-light: #29d5b0;
27 |   --ifm-color-primary-lighter: #32d8b4;
28 |   --ifm-color-primary-lightest: #4fddbf;
29 |   --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
30 | }
31 | 
32 | .navbar--github-link {
33 |   width: 32px;
34 |   height: 32px;
35 |   padding: 6px;
36 |   margin-right: 20px;
37 |   margin-left: 6px;
38 |   border-radius: 50%;
39 |   transition: background var(--ifm-transition-fast);
40 | }
41 | 
42 | .navbar--github-link:hover {
43 |   background: var(--ifm-color-emphasis-200);
44 | }
45 | 
46 | .navbar--github-link:before {
47 |   content: "";
48 |   height: 100%;
49 |   display: block;
50 |   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")
51 |     no-repeat;
52 | }
53 | 
54 | html[data-theme="dark"] .navbar--github-link:before {
55 |   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")
56 |     no-repeat;
57 | }
58 | 
```
--------------------------------------------------------------------------------
/website/src/pages/styles.module.css:
--------------------------------------------------------------------------------
```css
  1 | .hero {
  2 |   padding: 4rem 0 6rem;
  3 |   text-align: center;
  4 | }
  5 | 
  6 | .container {
  7 |   max-width: 1200px;
  8 |   margin: 0 auto;
  9 |   padding: 0 1rem;
 10 |   display: flex;
 11 |   flex-direction: column;
 12 |   align-items: center;
 13 | }
 14 | 
 15 | .heroBanner {
 16 |   display: flex;
 17 |   flex-direction: row;
 18 |   align-items: center;
 19 |   gap: 2rem;
 20 | }
 21 | 
 22 | .heroContent {
 23 |   flex: 1;
 24 |   margin-bottom: 2.5rem;
 25 |   max-width: 800px;
 26 | }
 27 | 
 28 | .heroTitle {
 29 |   font-size: 4rem;
 30 |   margin-bottom: 1.5rem;
 31 |   font-weight: 800;
 32 |   letter-spacing: -0.05em;
 33 |   line-height: 1.1;
 34 | }
 35 | 
 36 | .heroSubtitle {
 37 |   font-size: 1.25rem;
 38 |   margin-bottom: 2rem;
 39 |   opacity: 0.8;
 40 |   font-weight: 400;
 41 |   line-height: 1.6;
 42 |   letter-spacing: -0.015em;
 43 |   max-width: 700px;
 44 |   margin-left: auto;
 45 |   margin-right: auto;
 46 | }
 47 | 
 48 | .videoContainer {
 49 |   flex: 1;
 50 |   width: 80%;
 51 |   max-width: 900px;
 52 |   border-radius: 16px;
 53 |   overflow: hidden;
 54 |   box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
 55 |   position: relative;
 56 |   margin: 0 auto 3rem;
 57 |   transition: transform 0.3s ease;
 58 | }
 59 | 
 60 | .thumbnailContainer {
 61 |   position: relative;
 62 |   cursor: pointer;
 63 |   width: 100%;
 64 |   transform-style: preserve-3d;
 65 |   transform: perspective(1000px);
 66 |   will-change: transform;
 67 |   transition: transform 0.3s ease;
 68 | }
 69 | 
 70 | .thumbnailContainer:hover {
 71 |   transform: none;
 72 | }
 73 | 
 74 | .thumbnail {
 75 |   width: 100%;
 76 |   height: auto;
 77 |   display: block;
 78 |   border-radius: 16px;
 79 | }
 80 | 
 81 | .videoFrame {
 82 |   width: 100%;
 83 |   height: 500px;
 84 |   display: block;
 85 |   border-radius: 16px;
 86 | }
 87 | 
 88 | .playButtonContainer {
 89 |   position: absolute;
 90 |   top: 50%;
 91 |   left: 50%;
 92 |   transform: translate(-50%, -50%);
 93 | }
 94 | 
 95 | .playButtonOuter {
 96 |   height: 90px;
 97 |   width: 90px;
 98 |   background: rgba(255, 255, 255, 0.8);
 99 |   border-radius: 50%;
100 |   display: flex;
101 |   align-items: center;
102 |   justify-content: center;
103 |   transition: transform 0.2s ease;
104 | }
105 | 
106 | .playButtonInner {
107 |   height: 70px;
108 |   width: 70px;
109 |   background: black;
110 |   border-radius: 50%;
111 |   display: flex;
112 |   align-items: center;
113 |   justify-content: center;
114 | }
115 | 
116 | .playIcon {
117 |   width: 0;
118 |   height: 0;
119 |   border-style: solid;
120 |   border-width: 12px 0 12px 24px;
121 |   border-color: transparent transparent transparent white;
122 |   margin-left: 5px;
123 | }
124 | 
125 | .thumbnailContainer:hover .playButtonOuter {
126 |   transform: scale(1.1);
127 | }
128 | 
129 | .buttonContainer {
130 |   margin-top: 1rem;
131 |   display: flex;
132 |   justify-content: center;
133 | }
134 | 
135 | .tryButton {
136 |   display: inline-block;
137 |   background-color: #000;
138 |   color: #fff;
139 |   font-size: 1.1rem;
140 |   font-weight: 600;
141 |   padding: 14px 32px;
142 |   border-radius: 50px;
143 |   text-decoration: none;
144 |   transition: all 0.2s ease;
145 |   border: 2px solid #000;
146 | }
147 | 
148 | .tryButton:hover {
149 |   background-color: #fff;
150 |   color: #000;
151 |   text-decoration: none;
152 | }
153 | 
154 | /* Dark theme styles */
155 | html[data-theme='dark'] .tryButton {
156 |   background-color: #fff;
157 |   color: #000;
158 |   border: 2px solid #fff;
159 | }
160 | 
161 | html[data-theme='dark'] .tryButton:hover {
162 |   background-color: transparent;
163 |   color: #fff;
164 |   border: 2px solid #fff;
165 | }
166 | 
167 | @media screen and (max-width: 996px) {
168 |   .heroBanner {
169 |     flex-direction: column;
170 |     text-align: center;
171 |   }
172 |   
173 |   .heroTitle {
174 |     font-size: 3rem;
175 |   }
176 |   
177 |   .heroSubtitle {
178 |     font-size: 1.1rem;
179 |   }
180 | 
181 |   .videoFrame {
182 |     height: 300px;
183 |   }
184 |   
185 |   .videoContainer {
186 |     width: 95%;
187 |   }
188 |   
189 |   .playButtonOuter {
190 |     height: 70px;
191 |     width: 70px;
192 |   }
193 |   
194 |   .playButtonInner {
195 |     height: 50px;
196 |     width: 50px;
197 |   }
198 |   
199 |   .playIcon {
200 |     border-width: 10px 0 10px 18px;
201 |   }
202 | }
203 | 
```
--------------------------------------------------------------------------------
/website/src/pages/index.js:
--------------------------------------------------------------------------------
```javascript
  1 | import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
  2 | import Layout from "@theme/Layout";
  3 | import React, { useState, useRef, useEffect } from "react";
  4 | import Link from '@docusaurus/Link';
  5 | import styles from "./styles.module.css";
  6 | 
  7 | function VideoPlayer() {
  8 |   const [showVideo, setShowVideo] = useState(false);
  9 |   const thumbnailRef = useRef(null);
 10 |   const [tiltStyle, setTiltStyle] = useState({});
 11 |   
 12 |   const playVideo = () => {
 13 |     setShowVideo(true);
 14 |   };
 15 | 
 16 |   useEffect(() => {
 17 |     const container = thumbnailRef.current;
 18 |     if (!container) return;
 19 | 
 20 |     const handleMouseMove = (e) => {
 21 |       if (showVideo) return;
 22 |       
 23 |       const rect = container.getBoundingClientRect();
 24 |       const x = e.clientX - rect.left; // x position within the element
 25 |       const y = e.clientY - rect.top;  // y position within the element
 26 |       
 27 |       // Calculate the tilt angle based on mouse position
 28 |       // The further from center, the more tilt (up to max degrees)
 29 |       const centerX = rect.width / 2;
 30 |       const centerY = rect.height / 2;
 31 |       
 32 |       const maxTiltDegrees = 5; // Maximum tilt in degrees
 33 |       const tiltX = ((y - centerY) / centerY) * -maxTiltDegrees;
 34 |       const tiltY = ((x - centerX) / centerX) * maxTiltDegrees;
 35 |       
 36 |       setTiltStyle({
 37 |         transform: `perspective(1000px) rotateX(${tiltX}deg) rotateY(${tiltY}deg)`,
 38 |         transition: 'transform 0.05s ease-out'
 39 |       });
 40 |     };
 41 |     
 42 |     const handleMouseLeave = () => {
 43 |       setTiltStyle({
 44 |         transform: 'perspective(1000px) rotateX(0deg) rotateY(0deg)',
 45 |         transition: 'transform 0.5s ease-out'
 46 |       });
 47 |     };
 48 | 
 49 |     container.addEventListener('mousemove', handleMouseMove);
 50 |     container.addEventListener('mouseleave', handleMouseLeave);
 51 | 
 52 |     return () => {
 53 |       container.removeEventListener('mousemove', handleMouseMove);
 54 |       container.removeEventListener('mouseleave', handleMouseLeave);
 55 |     };
 56 |   }, [showVideo]);
 57 |   
 58 |   return (
 59 |     <div className={styles.videoContainer}>
 60 |       {!showVideo ? (
 61 |         <div 
 62 |           ref={thumbnailRef}
 63 |           className={styles.thumbnailContainer} 
 64 |           onClick={playVideo}
 65 |           style={tiltStyle}
 66 |         >
 67 |           <img 
 68 |             className={styles.thumbnail} 
 69 |             src="/img/lokka-intro-video.png" 
 70 |             alt="Lokka Demo - Introducing Lokka" 
 71 |           />
 72 |           <div className={styles.playButtonContainer}>
 73 |             <div className={styles.playButtonOuter}>
 74 |               <div className={styles.playButtonInner}>
 75 |                 <div className={styles.playIcon}></div>
 76 |               </div>
 77 |             </div>
 78 |           </div>
 79 |         </div>
 80 |       ) : (
 81 |         <iframe 
 82 |           className={styles.videoFrame}
 83 |           src="https://www.youtube.com/embed/f-ECqQSpLCM?autoplay=1"
 84 |           title="Lokka Demo - Introducing Lokka"
 85 |           frameBorder="0"
 86 |           allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
 87 |           allowFullScreen
 88 |         ></iframe>
 89 |       )}
 90 |     </div>
 91 |   );
 92 | }
 93 | 
 94 | export default function Home() {
 95 |   const { siteConfig } = useDocusaurusContext();
 96 |   return (
 97 |     <Layout
 98 |       title="Lokka"
 99 |       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.">
100 |       <main>
101 |         <div className={styles.hero}>
102 |           <div className={styles.container}>
103 |             <div className={styles.heroContent}>
104 |               <h1 className={styles.heroTitle}>Lokka</h1>
105 |               <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>
106 |                 <p className={styles.heroSubtitle}>Get a glimpse into the future of administering Microsoft 365 👇</p>
107 |             </div>
108 |             <VideoPlayer />
109 |             <div className={styles.buttonContainer}>
110 |               <Link
111 |                 className={styles.tryButton}
112 |                 to="/docs/install">
113 |                 Try Lokka
114 |               </Link>
115 |             </div>
116 |           </div>
117 |         </div>
118 |       </main>
119 |     </Layout>
120 |   );
121 | }
122 | 
```
--------------------------------------------------------------------------------
/website/docusaurus.config.js:
--------------------------------------------------------------------------------
```javascript
  1 | // @ts-check
  2 | // `@type` JSDoc annotations allow editor autocompletion and type checking
  3 | // (when paired with `@ts-check`).
  4 | // There are various equivalent ways to declare your Docusaurus config.
  5 | // See: https://docusaurus.io/docs/api/docusaurus-config
  6 | 
  7 | import { themes as prismThemes } from "prism-react-renderer";
  8 | 
  9 | // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
 10 | 
 11 | /** @type {import('@docusaurus/types').Config} */
 12 | const config = {
 13 |   title: "Lokka",
 14 |   tagline:
 15 |     "Beyond Commands, Beyond Clicks. A glimpse into the future of managing Microsoft 365 with AI!",
 16 |   favicon: "img/favicon.ico",
 17 | 
 18 |   // Set the production url of your site here
 19 |   url: "https://lokka.dev",
 20 |   // Set the /<baseUrl>/ pathname under which your site is served
 21 |   // For GitHub pages deployment, it is often '/<projectName>/'
 22 |   baseUrl: "/",
 23 | 
 24 |   // GitHub pages deployment config.
 25 |   // If you aren't using GitHub pages, you don't need these.
 26 |   organizationName: "merill", // Usually your GitHub org/user name.
 27 |   projectName: "lokka", // Usually your repo name.
 28 | 
 29 |   onBrokenLinks: "ignore",
 30 |   onBrokenMarkdownLinks: "warn",
 31 | 
 32 |   // Even if you don't use internationalization, you can use this field to set
 33 |   // useful metadata like html lang. For example, if your site is Chinese, you
 34 |   // may want to replace "en" with "zh-Hans".
 35 |   i18n: {
 36 |     defaultLocale: "en",
 37 |     locales: ["en"],
 38 |   },
 39 | 
 40 |   presets: [
 41 |     [
 42 |       "classic",
 43 |       /** @type {import('@docusaurus/preset-classic').Options} */
 44 |       ({
 45 |         docs: {
 46 |           sidebarPath: "./sidebars.js",
 47 |           // Please change this to your repo.
 48 |           // Remove this to remove the "edit this page" links.
 49 |           editUrl: "https://github.com/merill/lokka/tree/main/website/",
 50 |         },
 51 |         blog: {
 52 |           showReadingTime: true,
 53 |           feedOptions: {
 54 |             type: ["rss", "atom"],
 55 |             xslt: true,
 56 |           },
 57 |           // Please change this to your repo.
 58 |           // Remove this to remove the "edit this page" links.
 59 |           editUrl: "https://github.com/merill/lokka/tree/main/",
 60 |           // Useful options to enforce blogging best practices
 61 |           onInlineTags: "warn",
 62 |           onInlineAuthors: "warn",
 63 |           onUntruncatedBlogPosts: "warn",
 64 |         },
 65 |         theme: {
 66 |           customCss: "./src/css/custom.css",
 67 |         },
 68 |       }),
 69 |     ],
 70 |   ],
 71 | 
 72 |   themeConfig:
 73 |     /** @type {import('@docusaurus/preset-classic').ThemeConfig} */
 74 |     ({
 75 |       // Replace with your project's social card
 76 |       image: "img/docusaurus-social-card.png",
 77 |       navbar: {
 78 |         title: "Lokka",
 79 |         logo: {
 80 |           alt: "Lokka logo",
 81 |           src: "img/logo.svg",
 82 |         },
 83 |         items: [
 84 |           {
 85 |             type: "docSidebar",
 86 |             sidebarId: "siteSidebar",
 87 |             position: "left",
 88 |             label: "Docs",
 89 |           },
 90 |           {
 91 |             href: "https://merill.net",
 92 |             label: "merill.net",
 93 |             position: "right",
 94 |           },
 95 |           {
 96 |             "aria-label": "GitHub Repository",
 97 |             className: "navbar--github-link",
 98 |             href: "https://github.com/merill/lokka",
 99 |             position: "right",
100 |           },
101 |         ],
102 |       },
103 |       footer: {
104 |         style: "dark",
105 |         links: [
106 |           {
107 |             title: "My M365 tools",
108 |             items: [
109 |               {
110 |                 href: "https://graphxray.merill.net",
111 |                 label: "Graph X-Ray",
112 |                 position: "right",
113 |               },
114 |               {
115 |                 href: "https://graphpermissions.merill.net",
116 |                 label: "Graph Permissions Explorer",
117 |                 position: "right",
118 |               },
119 |               {
120 |                 href: "https://maester.dev",
121 |                 label: "Maester",
122 |                 position: "right",
123 |               },
124 |             ],
125 |           },
126 |           {
127 |             title: "My other tools",
128 |             items: [
129 |               {
130 |                 href: "https://cmd.ms",
131 |                 label: "cmd.ms",
132 |                 position: "right",
133 |               },
134 |               {
135 |                 href: "https://akasearch.net",
136 |                 label: "akasearch.net",
137 |                 position: "right",
138 |               },
139 |               {
140 |                 href: "https://mc.merill.net",
141 |                 label: "Message Center Archive",
142 |                 position: "right",
143 |               },
144 |             ],
145 |           },
146 |           {
147 |             title: "Follow Me",
148 |             items: [
149 |               {
150 |                 label: "LinkedIn",
151 |                 href: "https://linkedin.com/in/merill",
152 |               },
153 |               {
154 |                 label: "Bluesky",
155 |                 href: "https://bsky.app/profile/merill.net",
156 |               },
157 |               {
158 |                 label: "X",
159 |                 href: "https://x.com/merill",
160 |               },
161 |             ],
162 |           },
163 |           {
164 |             title: "My Entra specials",
165 |             items: [
166 |               {
167 |                 label: "Entra.News - My weekly newsletter",
168 |                 href: "https://entra.news",
169 |               },
170 |               {
171 |                 label: "Entra.Chat - My weekly podcast",
172 |                 href: "https://entra.chat",
173 |               },
174 |               {
175 |                 label: "idPowerToys",
176 |                 href: "https://idpowerapp.com",
177 |               },
178 |             ],
179 |           },
180 |         ],
181 |         copyright: `Copyright © ${new Date().getFullYear()} Merill Fernando.`,
182 |       },
183 |       prism: {
184 |         theme: prismThemes.github,
185 |         darkTheme: prismThemes.dracula,
186 |       },
187 |     }),
188 | };
189 | 
190 | export default config;
191 | 
```
--------------------------------------------------------------------------------
/website/docs/developer-guide.md:
--------------------------------------------------------------------------------
```markdown
  1 | ---
  2 | title: 🧩 Developer guide
  3 | sidebar_position: 4
  4 | ---
  5 | import Tabs from '@theme/Tabs';
  6 | import TabItem from '@theme/TabItem';
  7 | 
  8 | Follow this guide if you want to build Lokka from source to contribute to the project.
  9 | 
 10 | ## Pre-requisites
 11 | 
 12 | - Follow the [installation guide](install) to install Node and the [advanced guide](install-advanced) if you wish to create a custom Entra application.
 13 | - Clone the Lokka repository from GitHub [https://github.com/merill/lokka](https://github.com/merill/lokka)
 14 | 
 15 | ## Building the project
 16 | 
 17 | - Open a terminal and navigate to the Lokka project directory.
 18 | - Change into the folder `\src\mcp\`
 19 | - Run the following command to install the dependencies:
 20 | 
 21 |   ```bash
 22 |   npm install
 23 |   ```
 24 | 
 25 | - After the dependencies are installed, run the following command to build the project:
 26 | 
 27 |   ```bash
 28 |   npm run build
 29 |   ```
 30 | - When the build is complete, you will see a main.js file find the compiled files in the `\src\mcp\build\` folder.
 31 | 
 32 | ## Configuring the agent
 33 |   
 34 | <Tabs>
 35 |   <TabItem value="claude" label="Claude" default>
 36 | 
 37 | - In Claude Desktop, open the settings by clicking on the hamburger icon in the top left corner.
 38 | - Select **File** > **Settings** (or press `Ctrl + ,`)
 39 | - In the **Developer** tab, click **Edit Config**
 40 | - This opens explorer, edit `claude_desktop_config.json` in your favorite text editor.
 41 | - Add the following configuration to the file, using the information you in the **Overview** blade of the Entra application you created earlier.
 42 | 
 43 | - Note: On Windows the path needs to be escaped with `\\` or use `/` instead of `\`.
 44 |   - E.g. `C:\\Users\\<username>\\Documents\\lokka\\src\\mcp\\build\\main.js` or `C:/Users/<username>/Documents/lokka/src/mcp/build/main.js`
 45 | - Tip: Right-click on `build\main.js` in VS Code and select `Copy path` to copy the full path.
 46 | 
 47 | ```json
 48 | {
 49 |   "mcpServers": {
 50 |       "Lokka-Microsoft": {
 51 |           "command": "node",
 52 |           "args": [
 53 |               "<absolute-path-to-main.js>/src/mcp/build/main.js"
 54 |           ],
 55 |           "env": {
 56 |             "TENANT_ID": "<tenant-id>",
 57 |             "CLIENT_ID": "<client-id>",
 58 |             "CLIENT_SECRET": "<client-secret>"
 59 |           }
 60 |       }
 61 |   }
 62 | }
 63 | ```
 64 | 
 65 | - Exit Claude Desktop and restart it.
 66 |   - Every time you make changes to the code or configuration, you need to restart Claude desktop for the changes to take effect.
 67 |   - 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.
 68 | 
 69 | ### Testing the agent
 70 | 
 71 | #### Testing with Claude Desktop
 72 | 
 73 | - Open the Claude Desktop application.
 74 | - In the chat window on the bottom right you should see a hammer icon if the configuration is correct.
 75 | - Now you can start quering your Microsoft tenant using the Lokka agent tool.
 76 | - Some sample queries you can try are:
 77 |   - `Get all the users in my tenant`
 78 |   - `Show me the details for John Doe`
 79 |   - `Change John's department to IT` - Needs User.ReadWrite.All permission to be granted
 80 | 
 81 | </TabItem>
 82 | <TabItem value="vscode" label="VS Code">
 83 | 
 84 | ### Pre-requisites
 85 | 
 86 | - Install the latest version of [VS Code - Insider](https://code.visualstudio.com/insiders/)
 87 | - Install the latest version of [GitHub Copilot in VS Code](https://code.visualstudio.com/docs/copilot/setup)
 88 | 
 89 | ### VS Code
 90 | 
 91 | - In VS Code, open the Command Palette by pressing `Ctrl + Shift +P` (or `Cmd + Shift + P` on Mac).
 92 | - Type `MCP` and select `Command (stdio)`
 93 | - Select
 94 |   - Command: `node`
 95 |   - Server ID: `Lokka-Microsoft`
 96 | - Where to save configuration: `User Settings`
 97 | - This will open the `settings.json` file in VS Code.
 98 | 
 99 | - Add the following configuration to the file, using the information you in the **Overview** blade of the Entra application you created earlier.
100 | 
101 | - Note: On Windows the path needs to be escaped with `\\` or use `/` instead of `\`.
102 |   - E.g. `C:\\Users\\<username>\\Documents\\lokka\\src\\mcp\\build\\main.js` or `C:/Users/<username>/Documents/lokka/src/mcp/build/main.js`
103 | - Tip: Right-click on `build\main.js` in VS Code and select `Copy path` to copy the full path.
104 | 
105 | ```json
106 | "mcp": {
107 |   "servers": {
108 |       "Lokka-Microsoft": {
109 |           "command": "node",
110 |           "args": [
111 |               "<absolute-path-to-main.js>/src/mcp/build/main.js"
112 |           ],
113 |           "env": {
114 |             "TENANT_ID": "<tenant-id>",
115 |             "CLIENT_ID": "<client-id>",
116 |             "CLIENT_SECRET": "<client-secret>"
117 |           }
118 |       }
119 |   }
120 | }
121 | ```
122 | 
123 | - `File` > `Save` to save the file.
124 | 
125 | ### Testing the agent
126 | 
127 | - Start a new instance of VS Code (File > New Window)
128 | - Open `Copilot Edits` from `View` → `Copilot Edits`
129 | - At the bottom of the Copilot Edits panel (below the chat box)
130 |   - Select `Agent` (if it is showing `Edit`)
131 |   - Select `Claude 3.7 Sonnet` (if it is showing `GPT-40`)
132 | 
133 | </TabItem>
134 | </Tabs>
135 | 
136 | #### Testing with MCP Inspector
137 | 
138 | MCP 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.
139 | 
140 | See the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) for more information.
141 | 
142 | ```console
143 | npx @modelcontextprotocol/inspector node path/to/server/main.js args...
144 | ```
145 | 
146 | ## Learn about MCP
147 | 
148 | - [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.
149 | - [Model Context Protocol docs](https://modelcontextprotocol.io/introduction) - The official docs for the Model Context Protocol.
150 | - [Model Context Protocol Clients](https://modelcontextprotocol.io/clients) - List of all the clients that support the Model Context Protocol.
151 | 
```
--------------------------------------------------------------------------------
/website/docs/install.mdx:
--------------------------------------------------------------------------------
```markdown
  1 | ---
  2 | title: 🚀 Install
  3 | sidebar_position: 2
  4 | ---
  5 | import Tabs from '@theme/Tabs';
  6 | import TabItem from '@theme/TabItem';
  7 | 
  8 | # Install Lokka
  9 | 
 10 | This quick start guide will help you set up Lokka with the minimum configuration needed to get started.
 11 | 
 12 | If you want to learn more about the advanced configuration options, see the [Advanced Install Guide](/docs/install-advanced).
 13 | 
 14 | ## Pre-requisites
 15 | 
 16 | - Install [Node.js](https://nodejs.org/en/download/)
 17 |   - If you already have Node v22.10 or higher installed you can skip this step.
 18 |   - Check by running `node -v` at the command prompt.
 19 | 
 20 | ## Configure the agent
 21 | 
 22 | You can use the Lokka agent tool with any compatible MCP client and LLM combo.
 23 | 
 24 | Claude 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).
 25 | 
 26 | GitHub Copilot Agent in VS Code is another great option.
 27 | 
 28 | <Tabs>
 29 |   <TabItem value="claude" label="Claude" default>
 30 | 
 31 | ### Install Claude Desktop
 32 | 
 33 | - Download the latest version of Claude Desktop from [https://claude.ai/download](https://claude.ai/download)
 34 | - Install the application by following the instructions on the website.
 35 | - Open the application and sign in with your account (you can register for a free account).
 36 | 
 37 | ### Add Lokka to Claude Desktop
 38 | 
 39 | - In Claude Desktop, open Settings by clicking on the hamburger icon in the top left corner.
 40 | - Select **File** > **Settings** (or press `Ctrl + ,`)
 41 |   - On Mac, you can find the settings in the top menu bar under **Claude** > **Settings** (or press `Cmd + ,`).
 42 | - In the **Developer** tab, click **Edit Config**
 43 |   - Note: If you don't see the Developer tab, you need to enable it first from `Help` > `Enable Developer Mode`.
 44 | - This opens explorer, edit `claude_desktop_config.json` in your favorite text editor.
 45 | - Add the following configuration to the file.
 46 | 
 47 | ```json
 48 | {
 49 |   "mcpServers": {
 50 |     "Lokka-Microsoft": {
 51 |       "command": "npx",
 52 |       "args": ["-y", "@merill/lokka"]
 53 |     }
 54 |   }
 55 | }
 56 | ```
 57 | 
 58 | - Exit Claude Desktop and restart it.
 59 |   - Every time you make changes to the code or configuration, you need to restart Claude desktop for the changes to take effect.
 60 |   - 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.
 61 | 
 62 | ### Testing the agent
 63 | 
 64 | - Open the Claude Desktop application.
 65 | - You should see new browser window open and prompt you to sign into your Microsoft tenant.
 66 | - Now you can start quering your Microsoft tenant using the Lokka agent tool.
 67 | - Some sample queries you can try are:
 68 |   - `Get all users in my tenant`
 69 |   - `Show me the details for John Doe`
 70 |   - `Change John's department to IT` - Needs User.ReadWrite.All permission to be granted
 71 |   - `How many VMs do I have in my subscription?` - Needs Reader permission to be granted to the Azure subscription
 72 | 
 73 | </TabItem>
 74 |   <TabItem value="vscode" label="VS Code">
 75 | ### Pre-requisites
 76 | 
 77 | - Install the latest version of [VS Code](https://code.visualstudio.com)
 78 | 
 79 | ### Add Lokka to GitHub Copilot
 80 | 
 81 | <Tabs>
 82 |   <TabItem value="oneClickInstall" label="One Click Install" default>
 83 |   * Start **VS Code** and then click the button below to install Lokka in VS Code.
 84 |   * If your browser prompts you to open VS Code, click **Open**.
 85 |   * In the VS Code **Lokka-Microsoft** install page 
 86 |     * Click **Install**.
 87 |     * Click the widget icon next to the button and select **Start Server**.
 88 |   * This will open a browser window and prompt you to sign into your Microsoft tenant. 
 89 | 
 90 |   | Platform | VS Code | VS Code Insiders |
 91 |   | - | - | - |
 92 |   | Windows | [](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) | [](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) |
 93 |   | macOS/Linux | [](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) | [](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) |
 94 | 
 95 | :::note
 96 |   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.
 97 | :::
 98 | 
 99 |   </TabItem>
100 | 
101 |   <TabItem value="manualInstall" label="Manual Install" default>
102 | 
103 |     - In VS Code, open the Command Palette by pressing `Ctrl + Shift +P` (or `Cmd + Shift + P` on Mac).
104 |     - Type `MCP: Add` and select `MCP: Add Server...`
105 |     - Select `Command (stdio)`
106 |     - Command:
107 |       - Windows: `cmd /c npx -y @merill/lokka`
108 |       - macOS/Linux: `npx -y @merill/lokka`
109 |     - Name: `Lokka-Microsoft`
110 |     - Where to install: `Global`
111 |     - This will open the `settings.json` file in VS Code.
112 |     - `File` > `Save` to save the file.
113 |     - Once you hit save, you should see a browser window open and prompt you to sign into your Microsoft tenant.
114 |   </TabItem>
115 | </Tabs>
116 | 
117 | ### Starting the MCP server manually
118 | 
119 | Typically VSCode will automatically start the MCP server when needed, but you can also stop and start the MCP server manually.
120 | 
121 | - Open the Command Palette again (`Ctrl + Shift +P`) and type `MCP` and select `MCP: List Servers`
122 | - Select `Lokka-Microsoft` from the list of servers.
123 | - Selet `Start Server`
124 | - This will start the Lokka server 
125 | - Each time you hit Start you will see a browser window open and prompt you to sign into your Microsoft tenant.
126 |   - If you want to stay connected to the same tenant, you can use [AppApp-only authentication](/docs/install-advanced/app-only-auth).
127 | ### Testing the agent
128 | 
129 | - Start a new instance of VS Code (File > New Window)
130 | - Open `Chat` from `View` → `Chat`
131 | - At the bottom of the Chat panel (below the chat box)
132 |   - Select `Agent` (if it is showing `Ask` or `Edit`)
133 |   - Select `Claude Sonnet 4` or above (if it is showing `GPT-40`)
134 | 
135 | - Now you can start querying your Microsoft tenant using the Lokka agent tool.
136 | - Some sample queries you can try are:
137 |   - `Get all users in my tenant`
138 |   - `Show me the details for John Doe`
139 |   - `Change John's department to IT` - Needs User.ReadWrite.All permission to be granted
140 |   - `How many VMs do I have in my subscription?` - Needs Reader permission to be granted to the Azure subscription
141 | 
142 | :::note
143 |   If the chat prompts you to install VS GitHub Copilot for Azure, click the **rerun without** link to continue using Lokka.
144 | ::: 
145 | </TabItem>
146 | </Tabs>
147 | 
```
--------------------------------------------------------------------------------
/src/mcp/src/auth.ts:
--------------------------------------------------------------------------------
```typescript
  1 | import { AccessToken, TokenCredential, ClientSecretCredential, ClientCertificateCredential, InteractiveBrowserCredential, DeviceCodeCredential, DeviceCodeInfo } from "@azure/identity";
  2 | import { AuthenticationProvider } from "@microsoft/microsoft-graph-client";
  3 | import jwt from "jsonwebtoken";
  4 | import { logger } from "./logger.js";
  5 | import { LokkaClientId, LokkaDefaultTenantId, LokkaDefaultRedirectUri } from "./constants.js";
  6 | 
  7 | // Constants
  8 | const ONE_HOUR_IN_MS = 60 * 60 * 1000; // One hour in milliseconds
  9 | 
 10 | // Helper function to parse JWT and extract scopes
 11 | function parseJwtScopes(token: string): string[] {
 12 |   try {
 13 |     // Decode JWT without verifying signature (we trust the token from Azure Identity)
 14 |     const decoded = jwt.decode(token) as any;
 15 |     
 16 |     if (!decoded || typeof decoded !== 'object') {
 17 |       logger.info("Failed to decode JWT token");
 18 |       return [];
 19 |     }
 20 | 
 21 |     // Extract scopes from the 'scp' claim (space-separated string)
 22 |     const scopesString = decoded.scp;
 23 |     if (typeof scopesString === 'string') {
 24 |       return scopesString.split(' ').filter(scope => scope.length > 0);
 25 |     }
 26 | 
 27 |     // Some tokens might have roles instead of scopes
 28 |     const roles = decoded.roles;
 29 |     if (Array.isArray(roles)) {
 30 |       return roles;
 31 |     }
 32 | 
 33 |     logger.info("No scopes found in JWT token");
 34 |     return [];
 35 |   } catch (error) {
 36 |     logger.error("Error parsing JWT token for scopes", error);
 37 |     return [];
 38 |   }
 39 | }
 40 | 
 41 | // Simple authentication provider that works with Azure Identity TokenCredential
 42 | export class TokenCredentialAuthProvider implements AuthenticationProvider {
 43 |   private credential: TokenCredential;
 44 | 
 45 |   constructor(credential: TokenCredential) {
 46 |     this.credential = credential;
 47 |   }
 48 | 
 49 |   async getAccessToken(): Promise<string> {
 50 |     const token = await this.credential.getToken("https://graph.microsoft.com/.default");
 51 |     if (!token) {
 52 |       throw new Error("Failed to acquire access token");
 53 |     }
 54 |     return token.token;
 55 |   }
 56 | }
 57 | 
 58 | export interface TokenBasedCredential extends TokenCredential {
 59 |   getToken(scopes: string | string[]): Promise<AccessToken | null>;
 60 | }
 61 | 
 62 | export class ClientProvidedTokenCredential implements TokenBasedCredential {
 63 |   private accessToken: string | undefined;
 64 |   private expiresOn: Date | undefined;
 65 |   constructor(accessToken?: string, expiresOn?: Date) {
 66 |     if (accessToken) {
 67 |       this.accessToken = accessToken;
 68 |       this.expiresOn = expiresOn || new Date(Date.now() + ONE_HOUR_IN_MS); // Default 1 hour
 69 |     } else {
 70 |       this.expiresOn = new Date(0); // Set to epoch to indicate no valid token
 71 |     }
 72 |   }
 73 |   async getToken(scopes: string | string[]): Promise<AccessToken | null> {
 74 |     if (!this.accessToken || !this.expiresOn || this.expiresOn <= new Date()) {
 75 |       logger.error("Access token is not available or has expired");
 76 |       return null;
 77 |     }
 78 | 
 79 |     return {
 80 |       token: this.accessToken,
 81 |       expiresOnTimestamp: this.expiresOn.getTime()
 82 |     };
 83 |   }  updateToken(accessToken: string, expiresOn?: Date): void {
 84 |     this.accessToken = accessToken;
 85 |     this.expiresOn = expiresOn || new Date(Date.now() + ONE_HOUR_IN_MS);
 86 |     logger.info("Access token updated successfully");
 87 |   }
 88 |   isExpired(): boolean {
 89 |     return !this.expiresOn || this.expiresOn <= new Date();
 90 |   }
 91 | 
 92 |   getExpirationTime(): Date {
 93 |     return this.expiresOn || new Date(0);
 94 |   }
 95 | 
 96 |   // Getter for access token (for internal use by AuthManager)
 97 |   getAccessToken(): string | undefined {
 98 |     return this.accessToken;
 99 |   }
100 | }
101 | 
102 | export enum AuthMode {
103 |   ClientCredentials = "client_credentials",
104 |   ClientProvidedToken = "client_provided_token", 
105 |   Interactive = "interactive",
106 |   Certificate = "certificate"
107 | }
108 | 
109 | export interface AuthConfig {
110 |   mode: AuthMode;
111 |   tenantId?: string;
112 |   clientId?: string;
113 |   clientSecret?: string;
114 |   accessToken?: string;
115 |   expiresOn?: Date;
116 |   redirectUri?: string;
117 |   certificatePath?: string;
118 |   certificatePassword?: string;
119 | }
120 | 
121 | export class AuthManager {
122 |   private credential: TokenCredential | null = null;
123 |   private config: AuthConfig;
124 | 
125 |   constructor(config: AuthConfig) {
126 |     this.config = config;
127 |   }
128 | 
129 |   async initialize(): Promise<void> {
130 |     switch (this.config.mode) {
131 |       case AuthMode.ClientCredentials:
132 |         if (!this.config.tenantId || !this.config.clientId || !this.config.clientSecret) {
133 |           throw new Error("Client credentials mode requires tenantId, clientId, and clientSecret");
134 |         }
135 |         logger.info("Initializing Client Credentials authentication");
136 |         this.credential = new ClientSecretCredential(
137 |           this.config.tenantId,
138 |           this.config.clientId,
139 |           this.config.clientSecret
140 |         );
141 |         break;
142 | 
143 |       case AuthMode.ClientProvidedToken:
144 |         logger.info("Initializing Client Provided Token authentication");
145 |         this.credential = new ClientProvidedTokenCredential(
146 |           this.config.accessToken,
147 |           this.config.expiresOn
148 |         );
149 |         break;
150 |         
151 |       case AuthMode.Certificate:
152 |         if (!this.config.tenantId || !this.config.clientId || !this.config.certificatePath) {
153 |           throw new Error("Certificate mode requires tenantId, clientId, and certificatePath");
154 |         }
155 |         logger.info("Initializing Certificate authentication");
156 |         this.credential = new ClientCertificateCredential(this.config.tenantId, this.config.clientId, {
157 |           certificatePath: this.config.certificatePath,
158 |           certificatePassword: this.config.certificatePassword
159 |         });
160 |         break;
161 | 
162 |       case AuthMode.Interactive:
163 |         // Use defaults if not provided
164 |         const tenantId = this.config.tenantId || LokkaDefaultTenantId;
165 |         const clientId = this.config.clientId || LokkaClientId;
166 |         
167 |         logger.info(`Initializing Interactive authentication with tenant ID: ${tenantId}, client ID: ${clientId}`);
168 |         
169 |         try {
170 |           // Try Interactive Browser first
171 |           this.credential = new InteractiveBrowserCredential({
172 |             tenantId: tenantId,
173 |             clientId: clientId,
174 |             redirectUri: this.config.redirectUri || LokkaDefaultRedirectUri,
175 |           });
176 |         } catch (error) {
177 |           // Fallback to Device Code flow
178 |           logger.info("Interactive browser failed, falling back to device code flow");
179 |           this.credential = new DeviceCodeCredential({
180 |             tenantId: tenantId,
181 |             clientId: clientId,
182 |             userPromptCallback: (info: DeviceCodeInfo) => {
183 |               console.log(`\n🔐 Authentication Required:`);
184 |               console.log(`Please visit: ${info.verificationUri}`);
185 |               console.log(`And enter code: ${info.userCode}\n`);
186 |               return Promise.resolve();
187 |             },
188 |           });
189 |         }
190 |         break;
191 | 
192 |       default:
193 |         throw new Error(`Unsupported authentication mode: ${this.config.mode}`);
194 |     }
195 | 
196 |     // Test the credential
197 |     await this.testCredential();
198 |   }
199 | 
200 |   updateAccessToken(accessToken: string, expiresOn?: Date): void {
201 |     if (this.config.mode === AuthMode.ClientProvidedToken && this.credential instanceof ClientProvidedTokenCredential) {
202 |       this.credential.updateToken(accessToken, expiresOn);
203 |     } else {
204 |       throw new Error("Token update only supported in client provided token mode");
205 |     }
206 |   }
207 |   private async testCredential(): Promise<void> {
208 |     if (!this.credential) {
209 |       throw new Error("Credential not initialized");
210 |     }
211 | 
212 |     // Skip testing if ClientProvidedToken mode has no initial token
213 |     if (this.config.mode === AuthMode.ClientProvidedToken && !this.config.accessToken) {
214 |       logger.info("Skipping initial credential test as no token was provided at startup.");
215 |       return;
216 |     }
217 | 
218 |     try {
219 |       const token = await this.credential.getToken("https://graph.microsoft.com/.default");
220 |       if (!token) {
221 |         throw new Error("Failed to acquire token");
222 |       }
223 |       logger.info("Authentication successful");
224 |     } catch (error) {
225 |       logger.error("Authentication test failed", error);
226 |       throw error;
227 |     }
228 |   }
229 |   getGraphAuthProvider(): TokenCredentialAuthProvider {
230 |     if (!this.credential) {
231 |       throw new Error("Authentication not initialized");
232 |     }
233 | 
234 |     return new TokenCredentialAuthProvider(this.credential);
235 |   }
236 | 
237 |   getAzureCredential(): TokenCredential {
238 |     if (!this.credential) {
239 |       throw new Error("Authentication not initialized");
240 |     }
241 |     return this.credential;
242 |   }
243 | 
244 |   getAuthMode(): AuthMode {
245 |     return this.config.mode;
246 |   }
247 | 
248 |   isClientCredentials(): boolean {
249 |     return this.config.mode === AuthMode.ClientCredentials;
250 |   }
251 | 
252 |   isClientProvidedToken(): boolean {
253 |     return this.config.mode === AuthMode.ClientProvidedToken;
254 |   }
255 | 
256 |   isInteractive(): boolean {
257 |     return this.config.mode === AuthMode.Interactive;
258 |   }
259 | 
260 |   async getTokenStatus(): Promise<{ isExpired: boolean; expiresOn?: Date; scopes?: string[] }> {
261 |     if (this.credential instanceof ClientProvidedTokenCredential) {
262 |       const tokenStatus = {
263 |         isExpired: this.credential.isExpired(),
264 |         expiresOn: this.credential.getExpirationTime()
265 |       };
266 | 
267 |       // If we have a valid token, parse it to extract scopes
268 |       if (!tokenStatus.isExpired) {
269 |         const accessToken = this.credential.getAccessToken();
270 |         if (accessToken) {
271 |           try {
272 |             const scopes = parseJwtScopes(accessToken);
273 |             return {
274 |               ...tokenStatus,
275 |               scopes: scopes
276 |             };
277 |           } catch (error) {
278 |             logger.error("Error parsing token scopes in getTokenStatus", error);
279 |             return tokenStatus;
280 |           }
281 |         }
282 |       }
283 | 
284 |       return tokenStatus;
285 |     } else if (this.credential) {
286 |       // For other credential types, try to get a fresh token and parse it
287 |       try {
288 |         const accessToken = await this.credential.getToken("https://graph.microsoft.com/.default");
289 |         if (accessToken && accessToken.token) {
290 |           const scopes = parseJwtScopes(accessToken.token);
291 |           return {
292 |             isExpired: false,
293 |             expiresOn: new Date(accessToken.expiresOnTimestamp),
294 |             scopes: scopes
295 |           };
296 |         }
297 |       } catch (error) {
298 |         logger.error("Error getting token for scope parsing", error);
299 |       }
300 |     }
301 |     
302 |     return { isExpired: false };
303 |   }
304 | }
305 | 
```
--------------------------------------------------------------------------------
/src/mcp/src/main.ts:
--------------------------------------------------------------------------------
```typescript
  1 | #!/usr/bin/env node
  2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  4 | import { z } from "zod";
  5 | import { Client, PageIterator, PageCollection } from "@microsoft/microsoft-graph-client";
  6 | import fetch from 'isomorphic-fetch'; // Required polyfill for Graph client
  7 | import { logger } from "./logger.js";
  8 | import { AuthManager, AuthConfig, AuthMode } from "./auth.js";
  9 | import { LokkaClientId, LokkaDefaultTenantId, LokkaDefaultRedirectUri, getDefaultGraphApiVersion } from "./constants.js";
 10 | 
 11 | // Set up global fetch for the Microsoft Graph client
 12 | (global as any).fetch = fetch;
 13 | 
 14 | // Create server instance
 15 | const server = new McpServer({
 16 |   name: "Lokka-Microsoft",
 17 |   version: "0.2.0", // Updated version for token-based auth support
 18 | });
 19 | 
 20 | logger.info("Starting Lokka Multi-Microsoft API MCP Server (v0.2.0 - Token-Based Auth Support)");
 21 | 
 22 | // Initialize authentication and clients
 23 | let authManager: AuthManager | null = null;
 24 | let graphClient: Client | null = null;
 25 | 
 26 | // Check USE_GRAPH_BETA environment variable
 27 | const useGraphBeta = process.env.USE_GRAPH_BETA !== 'false'; // Default to true unless explicitly set to 'false'
 28 | const defaultGraphApiVersion = getDefaultGraphApiVersion();
 29 | 
 30 | logger.info(`Graph API default version: ${defaultGraphApiVersion} (USE_GRAPH_BETA=${process.env.USE_GRAPH_BETA || 'undefined'})`);
 31 | 
 32 | server.tool(
 33 |   "Lokka-Microsoft",
 34 |   "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\"'.",
 35 |   {
 36 |     apiType: z.enum(["graph", "azure"]).describe("Type of Microsoft API to query. Options: 'graph' for Microsoft Graph (Entra) or 'azure' for Azure Resource Management."),
 37 |     path: z.string().describe("The Azure or Graph API URL path to call (e.g. '/users', '/groups', '/subscriptions')"),
 38 |     method: z.enum(["get", "post", "put", "patch", "delete"]).describe("HTTP method to use"),
 39 |     apiVersion: z.string().optional().describe("Azure Resource Management API version (required for apiType Azure)"),
 40 |     subscriptionId: z.string().optional().describe("Azure Subscription ID (for Azure Resource Management)."),
 41 |     queryParams: z.record(z.string()).optional().describe("Query parameters for the request"),
 42 |     body: z.record(z.string(), z.any()).optional().describe("The request body (for POST, PUT, PATCH)"),
 43 |     graphApiVersion: z.enum(["v1.0", "beta"]).optional().default(defaultGraphApiVersion as "v1.0" | "beta").describe(`Microsoft Graph API version to use (default: ${defaultGraphApiVersion})`),
 44 |     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."),
 45 |     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)."),
 46 |   },
 47 |   async ({
 48 |     apiType,
 49 |     path,
 50 |     method,
 51 |     apiVersion,
 52 |     subscriptionId,
 53 |     queryParams,
 54 |     body,
 55 |     graphApiVersion,
 56 |     fetchAll,
 57 |     consistencyLevel
 58 |   }: {
 59 |     apiType: "graph" | "azure";
 60 |     path: string;
 61 |     method: "get" | "post" | "put" | "patch" | "delete";
 62 |     apiVersion?: string;
 63 |     subscriptionId?: string;
 64 |     queryParams?: Record<string, string>;
 65 |     body?: any;
 66 |     graphApiVersion: "v1.0" | "beta";
 67 |     fetchAll: boolean;
 68 |     consistencyLevel?: string;
 69 |   }) => {
 70 |     // Override graphApiVersion if USE_GRAPH_BETA is explicitly set to false
 71 |     const effectiveGraphApiVersion = !useGraphBeta ? "v1.0" : graphApiVersion;
 72 |     
 73 |     logger.info(`Executing Lokka-Microsoft tool with params: apiType=${apiType}, path=${path}, method=${method}, graphApiVersion=${effectiveGraphApiVersion}, fetchAll=${fetchAll}, consistencyLevel=${consistencyLevel}`);
 74 |     let determinedUrl: string | undefined;
 75 | 
 76 |     try {
 77 |       let responseData: any;
 78 | 
 79 |       // --- Microsoft Graph Logic ---
 80 |       if (apiType === 'graph') {
 81 |         if (!graphClient) {
 82 |           throw new Error("Graph client not initialized");
 83 |         }
 84 |         determinedUrl = `https://graph.microsoft.com/${effectiveGraphApiVersion}`; // For error reporting
 85 | 
 86 |         // Construct the request using the Graph SDK client
 87 |         let request = graphClient.api(path).version(effectiveGraphApiVersion);
 88 | 
 89 |         // Add query parameters if provided and not empty
 90 |         if (queryParams && Object.keys(queryParams).length > 0) {
 91 |           request = request.query(queryParams);
 92 |         }
 93 | 
 94 |         // Add ConsistencyLevel header if provided
 95 |         if (consistencyLevel) {
 96 |           request = request.header('ConsistencyLevel', consistencyLevel);
 97 |           logger.info(`Added ConsistencyLevel header: ${consistencyLevel}`);
 98 |         }
 99 | 
100 |         // Handle different methods
101 |         switch (method.toLowerCase()) {
102 |           case 'get':
103 |             if (fetchAll) {
104 |               logger.info(`Fetching all pages for Graph path: ${path}`);
105 |               // Fetch the first page to get context and initial data
106 |               const firstPageResponse: PageCollection = await request.get();
107 |               const odataContext = firstPageResponse['@odata.context']; // Capture context from first page
108 |               let allItems: any[] = firstPageResponse.value || []; // Initialize with first page's items
109 | 
110 |               // Callback function to process subsequent pages
111 |               const callback = (item: any) => {
112 |                 allItems.push(item);
113 |                 return true; // Return true to continue iteration
114 |               };
115 | 
116 |               // Create a PageIterator starting from the first response
117 |               const pageIterator = new PageIterator(graphClient, firstPageResponse, callback);
118 | 
119 |               // Iterate over all remaining pages
120 |               await pageIterator.iterate();
121 | 
122 |               // Construct final response with context and combined values under 'value' key
123 |               responseData = {
124 |                 '@odata.context': odataContext,
125 |                 value: allItems
126 |               };
127 |               logger.info(`Finished fetching all Graph pages. Total items: ${allItems.length}`);
128 | 
129 |             } else {
130 |               logger.info(`Fetching single page for Graph path: ${path}`);
131 |               responseData = await request.get();
132 |             }
133 |             break;
134 |           case 'post':
135 |             responseData = await request.post(body ?? {});
136 |             break;
137 |           case 'put':
138 |             responseData = await request.put(body ?? {});
139 |             break;
140 |           case 'patch':
141 |             responseData = await request.patch(body ?? {});
142 |             break;
143 |           case 'delete':
144 |             responseData = await request.delete(); // Delete often returns no body or 204
145 |             // Handle potential 204 No Content response
146 |             if (responseData === undefined || responseData === null) {
147 |               responseData = { status: "Success (No Content)" };
148 |             }
149 |             break;
150 |           default:
151 |             throw new Error(`Unsupported method: ${method}`);
152 |         }
153 |       }      // --- Azure Resource Management Logic (using direct fetch) ---
154 |       else { // apiType === 'azure'
155 |         if (!authManager) {
156 |           throw new Error("Auth manager not initialized");
157 |         }
158 |         determinedUrl = "https://management.azure.com"; // For error reporting
159 | 
160 |         // Acquire token for Azure RM
161 |         const azureCredential = authManager.getAzureCredential();
162 |         const tokenResponse = await azureCredential.getToken("https://management.azure.com/.default");
163 |         if (!tokenResponse || !tokenResponse.token) {
164 |           throw new Error("Failed to acquire Azure access token");
165 |         }
166 | 
167 |         // Construct the URL (similar to previous implementation)
168 |         let url = determinedUrl;
169 |         if (subscriptionId) {
170 |           url += `/subscriptions/${subscriptionId}`;
171 |         }
172 |         url += path;
173 | 
174 |         if (!apiVersion) {
175 |           throw new Error("API version is required for Azure Resource Management queries");
176 |         }
177 |         const urlParams = new URLSearchParams({ 'api-version': apiVersion });
178 |         if (queryParams) {
179 |           for (const [key, value] of Object.entries(queryParams)) {
180 |             urlParams.append(String(key), String(value));
181 |           }
182 |         }
183 |         url += `?${urlParams.toString()}`;
184 | 
185 |         // Prepare request options
186 |         const headers: Record<string, string> = {
187 |           'Authorization': `Bearer ${tokenResponse.token}`,
188 |           'Content-Type': 'application/json'
189 |         };
190 |         const requestOptions: RequestInit = {
191 |           method: method.toUpperCase(),
192 |           headers: headers
193 |         };
194 |         if (["POST", "PUT", "PATCH"].includes(method.toUpperCase())) {
195 |           requestOptions.body = body ? JSON.stringify(body) : JSON.stringify({});
196 |         }
197 | 
198 |         // --- Pagination Logic for Azure RM (Manual Fetch) ---
199 |         if (fetchAll && method === 'get') {
200 |           logger.info(`Fetching all pages for Azure RM starting from: ${url}`);
201 |           let allValues: any[] = [];
202 |           let currentUrl: string | null = url;
203 | 
204 |           while (currentUrl) {            logger.info(`Fetching Azure RM page: ${currentUrl}`);
205 |             // Re-acquire token for each page (Azure tokens might expire)
206 |             const azureCredential = authManager.getAzureCredential();
207 |             const currentPageTokenResponse = await azureCredential.getToken("https://management.azure.com/.default");
208 |             if (!currentPageTokenResponse || !currentPageTokenResponse.token) {
209 |               throw new Error("Failed to acquire Azure access token during pagination");
210 |             }
211 |             const currentPageHeaders = { ...headers, 'Authorization': `Bearer ${currentPageTokenResponse.token}` };
212 |             const currentPageRequestOptions: RequestInit = { method: 'GET', headers: currentPageHeaders };
213 | 
214 |             const pageResponse = await fetch(currentUrl, currentPageRequestOptions);
215 |             const pageText = await pageResponse.text();
216 |             let pageData: any;
217 |             try {
218 |               pageData = pageText ? JSON.parse(pageText) : {};
219 |             } catch (e) {
220 |               logger.error(`Failed to parse JSON from Azure RM page: ${currentUrl}`, pageText);
221 |               pageData = { rawResponse: pageText };
222 |             }
223 | 
224 |             if (!pageResponse.ok) {
225 |               logger.error(`API error on Azure RM page ${currentUrl}:`, pageData);
226 |               throw new Error(`API error (${pageResponse.status}) during Azure RM pagination on ${currentUrl}: ${JSON.stringify(pageData)}`);
227 |             }
228 | 
229 |             if (pageData.value && Array.isArray(pageData.value)) {
230 |               allValues = allValues.concat(pageData.value);
231 |             } else if (currentUrl === url && !pageData.nextLink) {
232 |               allValues.push(pageData);
233 |             } else if (currentUrl !== url) {
234 |               logger.info(`[Warning] Azure RM response from ${currentUrl} did not contain a 'value' array.`);
235 |             }
236 |             currentUrl = pageData.nextLink || null; // Azure uses nextLink
237 |           }
238 |           responseData = { allValues: allValues };
239 |           logger.info(`Finished fetching all Azure RM pages. Total items: ${allValues.length}`);
240 |         } else {
241 |           // Single page fetch for Azure RM
242 |           logger.info(`Fetching single page for Azure RM: ${url}`);
243 |           const apiResponse = await fetch(url, requestOptions);
244 |           const responseText = await apiResponse.text();
245 |           try {
246 |             responseData = responseText ? JSON.parse(responseText) : {};
247 |           } catch (e) {
248 |             logger.error(`Failed to parse JSON from single Azure RM page: ${url}`, responseText);
249 |             responseData = { rawResponse: responseText };
250 |           }
251 |           if (!apiResponse.ok) {
252 |             logger.error(`API error for Azure RM ${method} ${path}:`, responseData);
253 |             throw new Error(`API error (${apiResponse.status}) for Azure RM: ${JSON.stringify(responseData)}`);
254 |           }
255 |         }
256 |       }
257 | 
258 |       // --- Format and Return Result ---
259 |       // For all requests, format as text
260 |       let resultText = `Result for ${apiType} API (${apiType === 'graph' ? effectiveGraphApiVersion : apiVersion}) - ${method} ${path}:\n\n`;
261 |       resultText += JSON.stringify(responseData, null, 2); // responseData already contains the correct structure for fetchAll Graph case
262 | 
263 |       // Add pagination note if applicable (only for single page GET)
264 |       if (!fetchAll && method === 'get') {
265 |          const nextLinkKey = apiType === 'graph' ? '@odata.nextLink' : 'nextLink';
266 |          if (responseData && responseData[nextLinkKey]) { // Added check for responseData existence
267 |              resultText += `\n\nNote: More results are available. To retrieve all pages, add the parameter 'fetchAll: true' to your request.`;
268 |          }
269 |       }
270 | 
271 |       return {
272 |         content: [{ type: "text" as const, text: resultText }],
273 |       };
274 | 
275 |     } catch (error: any) {
276 |       logger.error(`Error in Lokka-Microsoft tool (apiType: ${apiType}, path: ${path}, method: ${method}):`, error); // Added more context to error log
277 |       // Try to determine the base URL even in case of error
278 |       if (!determinedUrl) {
279 |          determinedUrl = apiType === 'graph'
280 |            ? `https://graph.microsoft.com/${effectiveGraphApiVersion}`
281 |            : "https://management.azure.com";
282 |       }
283 |       // Include error body if available from Graph SDK error
284 |       const errorBody = error.body ? (typeof error.body === 'string' ? error.body : JSON.stringify(error.body)) : 'N/A';
285 |       return {
286 |         content: [{
287 |           type: "text",
288 |           text: JSON.stringify({
289 |             error: error instanceof Error ? error.message : String(error),
290 |             statusCode: error.statusCode || 'N/A', // Include status code if available from SDK error
291 |             errorBody: errorBody,
292 |             attemptedBaseUrl: determinedUrl
293 |           }),
294 |         }],
295 |         isError: true
296 |       };
297 |     }
298 |   },
299 | );
300 | 
301 | // Add token management tools
302 | server.tool(
303 |   "set-access-token",
304 |   "Set or update the access token for Microsoft Graph authentication. Use this when the MCP Client has obtained a fresh token through interactive authentication.",
305 |   {
306 |     accessToken: z.string().describe("The access token obtained from Microsoft Graph authentication"),
307 |     expiresOn: z.string().optional().describe("Token expiration time in ISO format (optional, defaults to 1 hour from now)")
308 |   },
309 |   async ({ accessToken, expiresOn }) => {
310 |     try {
311 |       const expirationDate = expiresOn ? new Date(expiresOn) : undefined;
312 |       
313 |       if (authManager?.getAuthMode() === AuthMode.ClientProvidedToken) {
314 |         authManager.updateAccessToken(accessToken, expirationDate);
315 |         
316 |         // Reinitialize the Graph client with the new token
317 |         const authProvider = authManager.getGraphAuthProvider();
318 |         graphClient = Client.initWithMiddleware({
319 |           authProvider: authProvider,
320 |         });
321 |         
322 |         return {
323 |           content: [{ 
324 |             type: "text" as const, 
325 |             text: "Access token updated successfully. You can now make Microsoft Graph requests on behalf of the authenticated user." 
326 |           }],
327 |         };
328 |       } else {
329 |         return {
330 |           content: [{ 
331 |             type: "text" as const, 
332 |             text: "Error: MCP Server is not configured for client-provided token authentication. Set USE_CLIENT_TOKEN=true in environment variables." 
333 |           }],
334 |           isError: true
335 |         };
336 |       }
337 |     } catch (error: any) {
338 |       logger.error("Error setting access token:", error);
339 |       return {
340 |         content: [{ 
341 |           type: "text" as const, 
342 |           text: `Error setting access token: ${error.message}` 
343 |         }],
344 |         isError: true
345 |       };
346 |     }
347 |   }
348 | );
349 | 
350 | server.tool(
351 |   "get-auth-status",
352 |   "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.",
353 |   {},
354 |   async () => {
355 |     try {
356 |       const authMode = authManager?.getAuthMode() || "Not initialized";
357 |       const isReady = authManager !== null;
358 |       const tokenStatus = authManager ? await authManager.getTokenStatus() : { isExpired: false };
359 |       
360 |       return {
361 |         content: [{ 
362 |           type: "text" as const, 
363 |           text: JSON.stringify({
364 |             authMode,
365 |             isReady,
366 |             supportsTokenUpdates: authMode === AuthMode.ClientProvidedToken,
367 |             tokenStatus: tokenStatus,
368 |             timestamp: new Date().toISOString()
369 |           }, null, 2)
370 |         }],
371 |       };
372 |     } catch (error: any) {
373 |       return {
374 |         content: [{ 
375 |           type: "text" as const, 
376 |           text: `Error checking auth status: ${error.message}` 
377 |         }],
378 |         isError: true
379 |       };
380 |     }
381 |   }
382 | );
383 | 
384 | // Add tool for requesting additional Graph permissions
385 | server.tool(
386 |   "add-graph-permission",
387 |   "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.",
388 |   {
389 |     scopes: z.array(z.string()).describe("Array of Microsoft Graph permission scopes to request (e.g., ['User.Read', 'Mail.ReadWrite', 'Directory.Read.All'])")
390 |   },
391 |   async ({ scopes }) => {
392 |     try {
393 |       // Check if we're in interactive mode
394 |       if (!authManager || authManager.getAuthMode() !== AuthMode.Interactive) {
395 |         const currentMode = authManager?.getAuthMode() || "Not initialized";
396 |         const clientId = process.env.CLIENT_ID;
397 |         
398 |         let errorMessage = `Error: add-graph-permission tool is only available in interactive authentication mode. Current mode: ${currentMode}.\n\n`;
399 |         
400 |         if (currentMode === AuthMode.ClientCredentials) {
401 |           errorMessage += `📋 To add permissions in Client Credentials mode:\n`;
402 |           errorMessage += `1. Open the Microsoft Entra admin center (https://entra.microsoft.com)\n`;
403 |           errorMessage += `2. Navigate to Applications > App registrations\n`;
404 |           errorMessage += `3. Find your application${clientId ? ` (Client ID: ${clientId})` : ''}\n`;
405 |           errorMessage += `4. Go to API permissions\n`;
406 |           errorMessage += `5. Click "Add a permission" and select Microsoft Graph\n`;
407 |           errorMessage += `6. Choose "Application permissions" and add the required scopes:\n`;
408 |           errorMessage += `   ${scopes.map(scope => `• ${scope}`).join('\n   ')}\n`;
409 |           errorMessage += `7. Click "Grant admin consent" to approve the permissions\n`;
410 |           errorMessage += `8. Restart the MCP server to use the new permissions`;
411 |         } else if (currentMode === AuthMode.ClientProvidedToken) {
412 |           errorMessage += `📋 To add permissions in Client Provided Token mode:\n`;
413 |           errorMessage += `1. Obtain a new access token that includes the required scopes:\n`;
414 |           errorMessage += `   ${scopes.map(scope => `• ${scope}`).join('\n   ')}\n`;
415 |           errorMessage += `2. When obtaining the token, ensure these scopes are included in the consent prompt\n`;
416 |           errorMessage += `3. Use the set-access-token tool to update the server with the new token\n`;
417 |           errorMessage += `4. The new token will include the additional permissions`;
418 |         } else {
419 |           errorMessage += `To use interactive permission requests, set USE_INTERACTIVE=true in environment variables and restart the server.`;
420 |         }
421 |         
422 |         return {
423 |           content: [{ 
424 |             type: "text" as const, 
425 |             text: errorMessage
426 |           }],
427 |           isError: true
428 |         };
429 |       }
430 | 
431 |       // Validate scopes array
432 |       if (!scopes || scopes.length === 0) {
433 |         return {
434 |           content: [{ 
435 |             type: "text" as const, 
436 |             text: "Error: At least one permission scope must be specified." 
437 |           }],
438 |           isError: true
439 |         };
440 |       }
441 | 
442 |       // Validate scope format (basic validation)
443 |       const invalidScopes = scopes.filter(scope => !scope.includes('.') || scope.trim() !== scope);
444 |       if (invalidScopes.length > 0) {
445 |         return {
446 |           content: [{ 
447 |             type: "text" as const, 
448 |             text: `Error: Invalid scope format detected: ${invalidScopes.join(', ')}. Scopes should be in format like 'User.Read' or 'Mail.ReadWrite'.` 
449 |           }],
450 |           isError: true
451 |         };
452 |       }
453 | 
454 |       logger.info(`Requesting additional Graph permissions: ${scopes.join(', ')}`);
455 | 
456 |       // Get current configuration with defaults for interactive auth
457 |       const tenantId = process.env.TENANT_ID || LokkaDefaultTenantId;
458 |       const clientId = process.env.CLIENT_ID || LokkaClientId;
459 |       const redirectUri = process.env.REDIRECT_URI || LokkaDefaultRedirectUri;
460 | 
461 |       logger.info(`Using tenant ID: ${tenantId}, client ID: ${clientId} for interactive authentication`);
462 | 
463 |       // Create a new interactive credential with the requested scopes
464 |       const { InteractiveBrowserCredential, DeviceCodeCredential } = await import("@azure/identity");
465 |       
466 |       // Clear any existing auth manager to force fresh authentication
467 |       authManager = null;
468 |       graphClient = null;
469 |       
470 |       // Request token with the new scopes - this will trigger interactive authentication
471 |       const scopeString = scopes.map(scope => `https://graph.microsoft.com/${scope}`).join(' ');
472 |       logger.info(`Requesting fresh token with scopes: ${scopeString}`);
473 |       
474 |       console.log(`\n🔐 Requesting Additional Graph Permissions:`);
475 |       console.log(`Scopes: ${scopes.join(', ')}`);
476 |       console.log(`You will be prompted to sign in to grant these permissions.\n`);
477 | 
478 |       let newCredential;
479 |       let tokenResponse;
480 |       
481 |       try {
482 |         // Try Interactive Browser first - create fresh instance each time
483 |         newCredential = new InteractiveBrowserCredential({
484 |           tenantId: tenantId,
485 |           clientId: clientId,
486 |           redirectUri: redirectUri,
487 |         });
488 |         
489 |         // Request token immediately after creating credential
490 |         tokenResponse = await newCredential.getToken(scopeString);
491 |         
492 |       } catch (error) {
493 |         // Fallback to Device Code flow
494 |         logger.info("Interactive browser failed, falling back to device code flow");
495 |         newCredential = new DeviceCodeCredential({
496 |           tenantId: tenantId,
497 |           clientId: clientId,
498 |           userPromptCallback: (info) => {
499 |             console.log(`\n🔐 Additional Permissions Required:`);
500 |             console.log(`Please visit: ${info.verificationUri}`);
501 |             console.log(`And enter code: ${info.userCode}`);
502 |             console.log(`Requested scopes: ${scopes.join(', ')}\n`);
503 |             return Promise.resolve();
504 |           },
505 |         });
506 |         
507 |         // Request token with device code credential
508 |         tokenResponse = await newCredential.getToken(scopeString);
509 |       }
510 | 
511 |       if (!tokenResponse) {
512 |         return {
513 |           content: [{ 
514 |             type: "text" as const, 
515 |             text: "Error: Failed to acquire access token with the requested scopes. Please check your permissions and try again." 
516 |           }],
517 |           isError: true
518 |         };
519 |       }
520 | 
521 |       // Create a completely new auth manager instance with the updated credential
522 |       const authConfig: AuthConfig = {
523 |         mode: AuthMode.Interactive,
524 |         tenantId,
525 |         clientId,
526 |         redirectUri
527 |       };
528 | 
529 |       // Create a new auth manager instance
530 |       authManager = new AuthManager(authConfig);
531 |       
532 |       // Manually set the credential to our new one with the additional scopes
533 |       (authManager as any).credential = newCredential;
534 | 
535 |       // DO NOT call initialize() as it might interfere with our fresh token
536 |       // Instead, directly create the Graph client with the new credential
537 |       const authProvider = authManager.getGraphAuthProvider();
538 |       graphClient = Client.initWithMiddleware({
539 |         authProvider: authProvider,
540 |       });
541 | 
542 |       // Get the token status to show the new scopes
543 |       const tokenStatus = await authManager.getTokenStatus();
544 | 
545 |       logger.info(`Successfully acquired fresh token with additional scopes: ${scopes.join(', ')}`);
546 | 
547 |       return {
548 |         content: [{ 
549 |           type: "text" as const, 
550 |           text: JSON.stringify({
551 |             message: "Successfully acquired additional Microsoft Graph permissions with fresh authentication",
552 |             requestedScopes: scopes,
553 |             tokenStatus: tokenStatus,
554 |             note: "A fresh sign-in was performed to ensure the new permissions are properly granted",
555 |             timestamp: new Date().toISOString()
556 |           }, null, 2)
557 |         }],
558 |       };
559 | 
560 |     } catch (error: any) {
561 |       logger.error("Error requesting additional Graph permissions:", error);
562 |       return {
563 |         content: [{ 
564 |           type: "text" as const, 
565 |           text: `Error requesting additional permissions: ${error.message}` 
566 |         }],
567 |         isError: true
568 |       };
569 |     }
570 |   }
571 | );
572 | 
573 | // Start the server with stdio transport
574 | async function main() {
575 |   // Determine authentication mode based on environment variables
576 |   const useCertificate = process.env.USE_CERTIFICATE === 'true';
577 |   const useInteractive = process.env.USE_INTERACTIVE === 'true';
578 |   const useClientToken = process.env.USE_CLIENT_TOKEN === 'true';
579 |   const initialAccessToken = process.env.ACCESS_TOKEN;
580 |   
581 |   let authMode: AuthMode;
582 |   
583 |   // Ensure only one authentication mode is enabled at a time
584 |   const enabledModes = [
585 |     useClientToken,
586 |     useInteractive,
587 |     useCertificate
588 |   ].filter(Boolean);
589 | 
590 |   if (enabledModes.length > 1) {
591 |     throw new Error(
592 |       "Multiple authentication modes enabled. Please enable only one of USE_CLIENT_TOKEN, USE_INTERACTIVE, or USE_CERTIFICATE."
593 |     );
594 |   }
595 | 
596 |   if (useClientToken) {
597 |     authMode = AuthMode.ClientProvidedToken;
598 |     if (!initialAccessToken) {
599 |       logger.info("Client token mode enabled but no initial token provided. Token must be set via set-access-token tool.");
600 |     }
601 |   } else if (useInteractive) {
602 |     authMode = AuthMode.Interactive;
603 |   } else if (useCertificate) {
604 |     authMode = AuthMode.Certificate;
605 |   } else {
606 |     // Check if we have client credentials environment variables
607 |     const hasClientCredentials = process.env.TENANT_ID && process.env.CLIENT_ID && process.env.CLIENT_SECRET;
608 |     
609 |     if (hasClientCredentials) {
610 |       authMode = AuthMode.ClientCredentials;
611 |     } else {
612 |       // Default to interactive mode for better user experience
613 |       authMode = AuthMode.Interactive;
614 |       logger.info("No authentication mode specified and no client credentials found. Defaulting to interactive mode.");
615 |     }
616 |   }
617 | 
618 |   logger.info(`Starting with authentication mode: ${authMode}`);
619 | 
620 |   // Get tenant ID and client ID with defaults only for interactive mode
621 |   let tenantId: string | undefined;
622 |   let clientId: string | undefined;
623 |   
624 |   if (authMode === AuthMode.Interactive) {
625 |     // Interactive mode can use defaults
626 |     tenantId = process.env.TENANT_ID || LokkaDefaultTenantId;
627 |     clientId = process.env.CLIENT_ID || LokkaClientId;
628 |     logger.info(`Interactive mode using tenant ID: ${tenantId}, client ID: ${clientId}`);
629 |   } else {
630 |     // All other modes require explicit values from environment variables
631 |     tenantId = process.env.TENANT_ID;
632 |     clientId = process.env.CLIENT_ID;
633 |   }
634 | 
635 |   const clientSecret = process.env.CLIENT_SECRET;
636 |   const certificatePath = process.env.CERTIFICATE_PATH;
637 |   const certificatePassword = process.env.CERTIFICATE_PASSWORD; // optional
638 | 
639 |   // Validate required configuration
640 |   if (authMode === AuthMode.ClientCredentials) {
641 |     if (!tenantId || !clientId || !clientSecret) {
642 |       throw new Error("Client credentials mode requires explicit TENANT_ID, CLIENT_ID, and CLIENT_SECRET environment variables");
643 |     }
644 |   } else if (authMode === AuthMode.Certificate) {
645 |     if (!tenantId || !clientId || !certificatePath) {
646 |       throw new Error("Certificate mode requires explicit TENANT_ID, CLIENT_ID, and CERTIFICATE_PATH environment variables");
647 |     }
648 |   }
649 |   // Note: Client token mode can start without a token and receive it later
650 | 
651 |   const authConfig: AuthConfig = {
652 |     mode: authMode,
653 |     tenantId,
654 |     clientId,
655 |     clientSecret,
656 |     accessToken: initialAccessToken,
657 |     redirectUri: process.env.REDIRECT_URI,
658 |     certificatePath,
659 |     certificatePassword
660 |   };
661 | 
662 |   authManager = new AuthManager(authConfig);
663 |   
664 |   // Only initialize if we have required config (for client token mode, we can start without a token)
665 |   if (authMode !== AuthMode.ClientProvidedToken || initialAccessToken) {
666 |     await authManager.initialize();
667 |     
668 |     // Initialize Graph Client
669 |     const authProvider = authManager.getGraphAuthProvider();
670 |     graphClient = Client.initWithMiddleware({
671 |       authProvider: authProvider,
672 |     });
673 |     
674 |     logger.info(`Authentication initialized successfully using ${authMode} mode`);
675 |   } else {
676 |     logger.info("Started in client token mode. Use set-access-token tool to provide authentication token.");
677 |   }
678 | 
679 |   const transport = new StdioServerTransport();
680 |   await server.connect(transport);
681 | }
682 | 
683 | main().catch((error) => {
684 |   console.error("Fatal error in main():", error);
685 |   logger.error("Fatal error in main()", error);
686 |   process.exit(1);
687 | });
688 | 
```