lkr-dev-cli 0.0.32__tar.gz → 0.0.34__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/.github/workflows/release.yml +6 -0
  2. lkr_dev_cli-0.0.34/Makefile +26 -0
  3. lkr_dev_cli-0.0.32/README.md → lkr_dev_cli-0.0.34/PKG-INFO +69 -5
  4. lkr_dev_cli-0.0.32/PKG-INFO → lkr_dev_cli-0.0.34/README.md +33 -43
  5. lkr_dev_cli-0.0.34/codemode.md +51 -0
  6. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/auth/main.py +15 -9
  7. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/auth_service.py +19 -2
  8. lkr_dev_cli-0.0.34/lkr/codemode/__init__.py +3 -0
  9. lkr_dev_cli-0.0.34/lkr/codemode/main.py +104 -0
  10. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/main.py +1 -0
  11. lkr_dev_cli-0.0.34/lkr/tools/main.py +232 -0
  12. lkr_dev_cli-0.0.34/lkr/tools/permission_deprecation.py +193 -0
  13. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr.md +51 -0
  14. lkr_dev_cli-0.0.34/pyproject.toml +67 -0
  15. lkr_dev_cli-0.0.34/tests/test_codemode.py +18 -0
  16. lkr_dev_cli-0.0.34/tests/test_codemode2.py +26 -0
  17. lkr_dev_cli-0.0.34/tests/test_permission_deprecation.py +176 -0
  18. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/uv.lock +80 -37
  19. lkr_dev_cli-0.0.32/Makefile +0 -7
  20. lkr_dev_cli-0.0.32/lkr/tools/main.py +0 -87
  21. lkr_dev_cli-0.0.32/pyproject.toml +0 -64
  22. lkr_dev_cli-0.0.32/test.py +0 -16
  23. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/.github/workflows/test-dependencies.yml +0 -0
  24. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/.gitignore +0 -0
  25. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/.python-version +0 -0
  26. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/.vscode/launch.json +0 -0
  27. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/.vscode/settings.json +0 -0
  28. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/Dockerfile +0 -0
  29. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/LICENSE +0 -0
  30. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/cloudbuild.yaml +0 -0
  31. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/__init__.py +0 -0
  32. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/auth/__init__.py +0 -0
  33. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/auth/oauth.py +0 -0
  34. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/classes.py +0 -0
  35. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/constants.py +0 -0
  36. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/custom_types.py +0 -0
  37. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/exceptions.py +0 -0
  38. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/logger.py +0 -0
  39. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/mcp/classes.py +0 -0
  40. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/mcp/main.py +0 -0
  41. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/mcp/utils.py +0 -0
  42. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/observability/classes.py +0 -0
  43. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/observability/embed_container.html +0 -0
  44. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/observability/main.py +0 -0
  45. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/observability/utils.py +0 -0
  46. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/tools/classes.py +0 -0
  47. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/tests/TESTING.md +0 -0
  48. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/tests/test_dependency_resolution.py +0 -0
  49. {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/tests/test_deps.sh +0 -0
@@ -52,6 +52,12 @@ jobs:
52
52
  ${{ env.IMAGE_NAME }}:latest \
53
53
  --project ${{ secrets.GCP_PROJECT_ID }}
54
54
 
55
+ - name: Trigger Website Deploy
56
+ run: |
57
+ curl -fSL "$DEPLOY_URL"
58
+ env:
59
+ DEPLOY_URL: ${{ secrets.DEPLOY_URL }}
60
+
55
61
  publish:
56
62
  name: python
57
63
  runs-on: ubuntu-latest
@@ -0,0 +1,26 @@
1
+ .PHONY: docs test-deps codemode-test codemode-test2 codemode-start
2
+
3
+ docs:
4
+ uv run typer lkr/main.py utils docs --output lkr.md
5
+
6
+ test-deps:
7
+ python tests/test_dependency_resolution.py
8
+
9
+ codemode-test:
10
+ uv run python tests/test_codemode.py
11
+
12
+ codemode-test2:
13
+ uv run python tests/test_codemode2.py
14
+
15
+
16
+ codemode-start:
17
+ @echo "Add this to your mcpServers config:"
18
+ @echo "{"
19
+ @echo " \"mcpServers\": {"
20
+ @echo " \"lkr-codemode\": {"
21
+ @echo " \"command\": \"uvx\","
22
+ @echo " \"args\": [\"--from\", \"lkr-dev-cli[codemode]\", \"lkr\", \"code-mode\", \"run\"]"
23
+ @echo " }"
24
+ @echo " }"
25
+ @echo "}"
26
+ uv run lkr code-mode run
@@ -1,10 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: lkr-dev-cli
3
+ Version: 0.0.34
4
+ Summary: lkr: a command line interface for looker
5
+ Author: bwebs
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.12
9
+ Requires-Dist: cryptography>=45.0.4
10
+ Requires-Dist: looker-sdk>=25.10.0
11
+ Requires-Dist: pydantic>=2.11.7
12
+ Requires-Dist: pydash>=8.0.5
13
+ Requires-Dist: questionary>=2.1.0
14
+ Requires-Dist: requests>=2.32.4
15
+ Requires-Dist: structlog>=25.4.0
16
+ Requires-Dist: typer>=0.16.0
17
+ Provides-Extra: all
18
+ Requires-Dist: duckdb>=1.3.1; extra == 'all'
19
+ Requires-Dist: fastapi[standard]>=0.115.14; extra == 'all'
20
+ Requires-Dist: mcp[cli]>=1.10.1; extra == 'all'
21
+ Requires-Dist: pydantic-monty; extra == 'all'
22
+ Requires-Dist: selenium>=4.34.0; extra == 'all'
23
+ Provides-Extra: codemode
24
+ Requires-Dist: mcp[cli]>=1.10.1; extra == 'codemode'
25
+ Requires-Dist: pydantic-monty; extra == 'codemode'
26
+ Provides-Extra: mcp
27
+ Requires-Dist: duckdb>=1.3.1; extra == 'mcp'
28
+ Requires-Dist: fastapi[standard]>=0.115.14; extra == 'mcp'
29
+ Requires-Dist: mcp[cli]>=1.10.1; extra == 'mcp'
30
+ Provides-Extra: observability
31
+ Requires-Dist: fastapi[standard]>=0.115.14; extra == 'observability'
32
+ Requires-Dist: selenium>=4.34.0; extra == 'observability'
33
+ Provides-Extra: tools
34
+ Requires-Dist: fastapi[standard]>=0.115.14; extra == 'tools'
35
+ Description-Content-Type: text/markdown
36
+
1
37
  # lkr cli
2
38
 
3
39
  The `lkr` cli is a tool for interacting with Looker. It combines Looker's SDK and customer logic to interact with Looker in meaninful ways. For a full list of commands, see the full [cli docs](./lkr.md)
4
40
 
5
41
  ## Usage
6
42
 
7
- `uv` makes everyone's life easier. Go [install it](https://docs.astral.sh/uv/getting-started/installation/). You can start using `lkr` by running `uv run --with lkr-dev-cli[all] lkr --help`.
43
+ `uv` makes everyone's life easier. Go [install it](https://docs.astral.sh/uv/getting-started/installation/). You can start using `lkr` by running `uvx --from lkr-dev-cli[all] lkr --help`.
8
44
 
9
45
  Alternatively, you can install `lkr` with `pip install lkr-dev-cli[all]` and use commands directly like `lkr <command>`.
10
46
 
@@ -24,7 +60,7 @@ See the [prerequisites section](#oauth2-prerequisites)
24
60
  Login to `lkr`
25
61
 
26
62
  ```bash
27
- uv run --with lkr-dev-cli[all] lkr auth login
63
+ uvx --from lkr-dev-cli[all] lkr auth login
28
64
  ```
29
65
 
30
66
  - Select a new instance
@@ -32,12 +68,12 @@ uv run --with lkr-dev-cli[all] lkr auth login
32
68
  - Choose whether you want this login to use production or development mode
33
69
  - Give it a name
34
70
 
35
- You will be redirected to the Looker OAuth authorization page, click Allow. If you do not see an allow button, the [prerequisites](#prerequisites) were not done properly.
71
+ You will be redirected to the Looker OAuth authorization page, click Allow. If you do not see an allow button, the [prerequisites](#oauth2-prerequisites) were not done properly.
36
72
 
37
73
  If everything is successful, you will see `Successfully authenticated!`. Test it with
38
74
 
39
75
  ```bash
40
- uv run --with lkr-dev-cli[all] lkr auth whoami
76
+ uvx --from lkr-dev-cli[all] lkr auth whoami
41
77
  ```
42
78
 
43
79
  ### Using API Key
@@ -45,7 +81,7 @@ uv run --with lkr-dev-cli[all] lkr auth whoami
45
81
  If you provide environment variables for `LOOKERSDK_CLIENT_ID`, `LOOKERSDK_CLIENT_SECRET`, and `LOOKERSDK_BASE_URL`, `lkr` will use the API key to authenticate and the commands. We also support command line arguments to pass in the client id, client secret, and base url.
46
82
 
47
83
  ```bash
48
- uv run --with lkr-dev-cli[all] lkr --client-id <your client id> --client-secret <your client secret> --base-url <your instance url> auth whoami
84
+ uvx --from lkr-dev-cli[all] lkr --client-id <your client id> --client-secret <your client secret> --base-url <your instance url> auth whoami
49
85
  ```
50
86
 
51
87
 
@@ -73,6 +109,10 @@ Go to the Looker API Explorer for Register OAuth App (https://your.looker.instan
73
109
  - This only needs to be done once per instance
74
110
 
75
111
 
112
+ ## Code-Mode
113
+
114
+ Execute Python code safely with full Looker SDK coverage within a secure sandbox environment. Constructed as an MCP tool, it dynamically inspects the Looker SDK for all public methods and injects them into the Monty sandbox safely. For detailed options, safe primitives transformations, and PKCE configurations, view the full [Code-Mode Docs](./codemode.md).
115
+
76
116
  ## MCP
77
117
  Built into the `lkr` is an MCP server. Right now its tools are based on helping you work within an IDE. To use it a tool like [Cursor](https://www.cursor.com/), add this to your mcp.json
78
118
 
@@ -325,6 +365,30 @@ def delete_user_attribute(user_attribute_name: str, email: str):
325
365
  )
326
366
  updater.delete_user_attribute_value()
327
367
 
368
+ ## Permission Deprecation Tool
369
+
370
+ The `schedule-download-deprecation` tool helps Looker admins ensure that users do not lose access to models they already have when Looker moves towards more granular model-specific permissions for scheduling and downloading.
371
+
372
+ ### How it helps
373
+ Currently, some permissions in Looker can be granted instance-wide. In the future, these permissions may need to be explicitly granted at the model level (via Model Sets). This tool audits all active users and identifies those who:
374
+ - Have "target permissions" (like `download_with_limit`, `schedule_look_emails`, etc.) instance-wide.
375
+ - Do **not** have those same permissions for specific models they otherwise have access to.
376
+
377
+ By running this tool, an admin can proactively identify and fix permission gaps before any deprecation takes effect, ensuring a seamless experience for end-users.
378
+
379
+ ### Usage
380
+ This command should be run by a **Looker Admin**.
381
+
382
+ ```bash
383
+ uvx --from lkr-dev-cli[all] lkr tools schedule-download-deprecation
384
+ ```
385
+
386
+ Options:
387
+ - `--csv`: Export the results to a CSV file for easier analysis of large instances.
388
+ - `--unfiltered`: Show all users, including those who have all required permissions across all models.
389
+ - `--model-offset`: Slice the table output to show different sets of models (the table shows 5 models at a time).
390
+
391
+
328
392
  ## Optional Dependencies
329
393
 
330
394
  The `lkr` CLI supports optional dependencies that enable additional functionality. You can install these individually or all at once.
@@ -1,48 +1,10 @@
1
- Metadata-Version: 2.4
2
- Name: lkr-dev-cli
3
- Version: 0.0.32
4
- Summary: lkr: a command line interface for looker
5
- Author: bwebs
6
- License-Expression: MIT
7
- License-File: LICENSE
8
- Requires-Python: >=3.12
9
- Requires-Dist: looker-sdk>=25.4.0
10
- Requires-Dist: pydantic>=2.11.4
11
- Requires-Dist: pydash>=8.0.5
12
- Provides-Extra: all
13
- Requires-Dist: cryptography>=42.0.0; extra == 'all'
14
- Requires-Dist: duckdb>=1.2.2; extra == 'all'
15
- Requires-Dist: fastapi[standard]>=0.115.12; extra == 'all'
16
- Requires-Dist: mcp[cli]>=1.9.2; extra == 'all'
17
- Requires-Dist: questionary>=2.1.0; extra == 'all'
18
- Requires-Dist: requests>=2.31.0; extra == 'all'
19
- Requires-Dist: selenium>=4.32.0; extra == 'all'
20
- Requires-Dist: structlog>=25.3.0; extra == 'all'
21
- Requires-Dist: typer>=0.15.2; extra == 'all'
22
- Provides-Extra: cli
23
- Requires-Dist: cryptography>=42.0.0; extra == 'cli'
24
- Requires-Dist: questionary>=2.1.0; extra == 'cli'
25
- Requires-Dist: requests>=2.31.0; extra == 'cli'
26
- Requires-Dist: structlog>=25.3.0; extra == 'cli'
27
- Requires-Dist: typer>=0.15.2; extra == 'cli'
28
- Provides-Extra: mcp
29
- Requires-Dist: duckdb>=1.2.2; extra == 'mcp'
30
- Requires-Dist: fastapi[standard]>=0.115.12; extra == 'mcp'
31
- Requires-Dist: mcp[cli]>=1.9.2; extra == 'mcp'
32
- Provides-Extra: observability
33
- Requires-Dist: fastapi[standard]>=0.115.12; extra == 'observability'
34
- Requires-Dist: selenium>=4.32.0; extra == 'observability'
35
- Provides-Extra: tools
36
- Requires-Dist: fastapi[standard]>=0.115.12; extra == 'tools'
37
- Description-Content-Type: text/markdown
38
-
39
1
  # lkr cli
40
2
 
41
3
  The `lkr` cli is a tool for interacting with Looker. It combines Looker's SDK and customer logic to interact with Looker in meaninful ways. For a full list of commands, see the full [cli docs](./lkr.md)
42
4
 
43
5
  ## Usage
44
6
 
45
- `uv` makes everyone's life easier. Go [install it](https://docs.astral.sh/uv/getting-started/installation/). You can start using `lkr` by running `uv run --with lkr-dev-cli[all] lkr --help`.
7
+ `uv` makes everyone's life easier. Go [install it](https://docs.astral.sh/uv/getting-started/installation/). You can start using `lkr` by running `uvx --from lkr-dev-cli[all] lkr --help`.
46
8
 
47
9
  Alternatively, you can install `lkr` with `pip install lkr-dev-cli[all]` and use commands directly like `lkr <command>`.
48
10
 
@@ -62,7 +24,7 @@ See the [prerequisites section](#oauth2-prerequisites)
62
24
  Login to `lkr`
63
25
 
64
26
  ```bash
65
- uv run --with lkr-dev-cli[all] lkr auth login
27
+ uvx --from lkr-dev-cli[all] lkr auth login
66
28
  ```
67
29
 
68
30
  - Select a new instance
@@ -70,12 +32,12 @@ uv run --with lkr-dev-cli[all] lkr auth login
70
32
  - Choose whether you want this login to use production or development mode
71
33
  - Give it a name
72
34
 
73
- You will be redirected to the Looker OAuth authorization page, click Allow. If you do not see an allow button, the [prerequisites](#prerequisites) were not done properly.
35
+ You will be redirected to the Looker OAuth authorization page, click Allow. If you do not see an allow button, the [prerequisites](#oauth2-prerequisites) were not done properly.
74
36
 
75
37
  If everything is successful, you will see `Successfully authenticated!`. Test it with
76
38
 
77
39
  ```bash
78
- uv run --with lkr-dev-cli[all] lkr auth whoami
40
+ uvx --from lkr-dev-cli[all] lkr auth whoami
79
41
  ```
80
42
 
81
43
  ### Using API Key
@@ -83,7 +45,7 @@ uv run --with lkr-dev-cli[all] lkr auth whoami
83
45
  If you provide environment variables for `LOOKERSDK_CLIENT_ID`, `LOOKERSDK_CLIENT_SECRET`, and `LOOKERSDK_BASE_URL`, `lkr` will use the API key to authenticate and the commands. We also support command line arguments to pass in the client id, client secret, and base url.
84
46
 
85
47
  ```bash
86
- uv run --with lkr-dev-cli[all] lkr --client-id <your client id> --client-secret <your client secret> --base-url <your instance url> auth whoami
48
+ uvx --from lkr-dev-cli[all] lkr --client-id <your client id> --client-secret <your client secret> --base-url <your instance url> auth whoami
87
49
  ```
88
50
 
89
51
 
@@ -111,6 +73,10 @@ Go to the Looker API Explorer for Register OAuth App (https://your.looker.instan
111
73
  - This only needs to be done once per instance
112
74
 
113
75
 
76
+ ## Code-Mode
77
+
78
+ Execute Python code safely with full Looker SDK coverage within a secure sandbox environment. Constructed as an MCP tool, it dynamically inspects the Looker SDK for all public methods and injects them into the Monty sandbox safely. For detailed options, safe primitives transformations, and PKCE configurations, view the full [Code-Mode Docs](./codemode.md).
79
+
114
80
  ## MCP
115
81
  Built into the `lkr` is an MCP server. Right now its tools are based on helping you work within an IDE. To use it a tool like [Cursor](https://www.cursor.com/), add this to your mcp.json
116
82
 
@@ -363,6 +329,30 @@ def delete_user_attribute(user_attribute_name: str, email: str):
363
329
  )
364
330
  updater.delete_user_attribute_value()
365
331
 
332
+ ## Permission Deprecation Tool
333
+
334
+ The `schedule-download-deprecation` tool helps Looker admins ensure that users do not lose access to models they already have when Looker moves towards more granular model-specific permissions for scheduling and downloading.
335
+
336
+ ### How it helps
337
+ Currently, some permissions in Looker can be granted instance-wide. In the future, these permissions may need to be explicitly granted at the model level (via Model Sets). This tool audits all active users and identifies those who:
338
+ - Have "target permissions" (like `download_with_limit`, `schedule_look_emails`, etc.) instance-wide.
339
+ - Do **not** have those same permissions for specific models they otherwise have access to.
340
+
341
+ By running this tool, an admin can proactively identify and fix permission gaps before any deprecation takes effect, ensuring a seamless experience for end-users.
342
+
343
+ ### Usage
344
+ This command should be run by a **Looker Admin**.
345
+
346
+ ```bash
347
+ uvx --from lkr-dev-cli[all] lkr tools schedule-download-deprecation
348
+ ```
349
+
350
+ Options:
351
+ - `--csv`: Export the results to a CSV file for easier analysis of large instances.
352
+ - `--unfiltered`: Show all users, including those who have all required permissions across all models.
353
+ - `--model-offset`: Slice the table output to show different sets of models (the table shows 5 models at a time).
354
+
355
+
366
356
  ## Optional Dependencies
367
357
 
368
358
  The `lkr` CLI supports optional dependencies that enable additional functionality. You can install these individually or all at once.
@@ -0,0 +1,51 @@
1
+ # Looker Code-Mode MCP Server
2
+
3
+ `lkr code-mode` allows you to invoke a Python-based Model Context Protocol (MCP) server. It offers an AI agent the unique capacity to batched-execute Python commands securely within the Monty sandbox against your active Looker instance!
4
+
5
+ ## How It Works
6
+
7
+ Instead of dumping hundreds of explicit tool declarations onto your AI agent (token bloat), `lkr code-mode` exposes **exactly one tool**: `run_python_code(code: str)`.
8
+
9
+ The tool instantiates Looker SDK natively, searches all bound methods, and passes them safely onto Monty's environment as global functions. When the LLM writes standard Python code (e.g., `me()`, `folder(id)`), Monty will process it correctly locally!
10
+
11
+ ### Key Features:
12
+ - **100% Tool Coverage:** Accesses all Looker SDK public operations smoothly without token limits.
13
+ - **Recursive Translation:** Complex Looker models like User, Folder, Dashboard get string-converted into dictionaries immediately before ingesting them into Monty.
14
+ - **Automatic PKCE Restarter:** Caught an invalid token? Code-Mode immediately catches `InvalidRefreshTokenError` and safely opens up your PKCE authentication browser automatically.
15
+ - **Extremely Secure:** Monty interpreter ensures isolated sandbox processing. No local filesystem accesses are exposed.
16
+
17
+ ## Continuous Usage
18
+
19
+ ### 1. Starting the Server
20
+ To immediately trigger the stdio listener, use:
21
+ ```bash
22
+ uvx --from lkr-dev-cli[codemode] lkr code-mode run
23
+ ```
24
+
25
+ ### 2. Client Configuration
26
+ To hook this server into Cursor or Claude Desktop natively over stdio, append the following onto your `mcpServers` configuration JSON. Make sure to pass your Looker instance credentials as environment variables (see standard API requirements in [README.md](./README.md#using-api-key)):
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "looker-codemode": {
32
+ "command": "uvx",
33
+ "args": ["--from", "lkr-dev-cli[codemode]", "lkr", "code-mode", "run"],
34
+ "env": {
35
+ "LOOKERSDK_BASE_URL": "https://your.looker.instance",
36
+ "LOOKERSDK_CLIENT_ID": "your-client-id",
37
+ "LOOKERSDK_CLIENT_SECRET": "your-client-secret"
38
+ }
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ ### 3. Visual Inspector
45
+ To check things out on a web panel:
46
+ ```bash
47
+ npx @modelcontextprotocol/inspector uvx --from lkr-dev-cli[codemode] lkr code-mode run
48
+ ```
49
+
50
+ ```
51
+
@@ -191,16 +191,22 @@ def whoami(ctx: typer.Context):
191
191
  Check current authentication
192
192
  """
193
193
  auth = get_auth(ctx)
194
- sdk = auth.get_current_sdk(prompt_refresh_invalid_token=True)
195
- if not sdk:
196
- logger.error(
197
- "Not currently authenticated - use `lkr auth login` or `lkr auth switch` to authenticate"
194
+ try:
195
+ sdk = auth.get_current_sdk(prompt_refresh_invalid_token=True)
196
+ if not sdk:
197
+ logger.error(
198
+ "Not currently authenticated - use `lkr auth login` or `lkr auth switch` to authenticate"
199
+ )
200
+ raise typer.Exit(1)
201
+ user = sdk.me()
202
+ logger.info(
203
+ f"Currently authenticated as {user.first_name} {user.last_name} ({user.email}) to {sdk.auth.settings.base_url}"
198
204
  )
199
- raise typer.Exit(1)
200
- user = sdk.me()
201
- logger.info(
202
- f"Currently authenticated as {user.first_name} {user.last_name} ({user.email}) to {sdk.auth.settings.base_url}"
203
- )
205
+ except Exception as e:
206
+ if "invalid_grant" in str(e) or "token expired" in str(e):
207
+ logger.error("Your Looker OAuth session has expired. Please run 'lkr auth login' to re-authenticate.")
208
+ raise typer.Exit(1)
209
+ raise e
204
210
 
205
211
 
206
212
  @group.command()
@@ -23,7 +23,12 @@ from lkr.constants import LOOKER_API_VERSION, OAUTH_CLIENT_ID, OAUTH_REDIRECT_UR
23
23
  from lkr.custom_types import NewTokenCallback
24
24
  from lkr.logger import logger
25
25
 
26
- __all__ = ["get_auth", "ApiKeyAuthSession", "DbOAuthSession"]
26
+ __all__ = ["get_auth", "ApiKeyAuthSession", "DbOAuthSession", "is_auth_expired"]
27
+
28
+
29
+ def is_auth_expired(e: Exception) -> bool:
30
+ return "invalid_grant" in str(e) or "token expired" in str(e)
31
+
27
32
 
28
33
 
29
34
  def get_auth(ctx: Union["typer.Context", LkrCtxObj]) -> Union["SqlLiteAuth", "ApiKeyAuth"]:
@@ -465,11 +470,23 @@ class SqlLiteAuth:
465
470
  def refresh_current_token(token: Union[AccessToken, AuthToken]):
466
471
  current_auth.set_token(self.conn, new_token=token, commit=True)
467
472
 
468
- return init_oauth_sdk(
473
+ sdk = init_oauth_sdk(
469
474
  current_auth.base_url,
470
475
  new_token_callback=refresh_current_token,
471
476
  access_token=current_auth.to_access_token(),
472
477
  )
478
+ if prompt_refresh_invalid_token:
479
+ import sys
480
+ try:
481
+ sdk.auth.authenticate({})
482
+ except Exception as e:
483
+ if is_auth_expired(e):
484
+ if sys.stdin.isatty():
485
+ self._cli_confirm_refresh_token(current_auth, quiet=False)
486
+ return self.get_current_sdk(prompt_refresh_invalid_token=False)
487
+ raise e
488
+
489
+ return sdk
473
490
 
474
491
  else:
475
492
  logger.error("No current instance found, please login")
@@ -0,0 +1,3 @@
1
+ from .main import group
2
+
3
+ __all__ = ["group"]
@@ -0,0 +1,104 @@
1
+ import inspect
2
+ from typing import Optional
3
+
4
+ import typer
5
+ import pydantic_monty
6
+ from mcp.server.fastmcp import FastMCP
7
+
8
+ from lkr.auth_service import get_auth, is_auth_expired
9
+ from lkr.classes import LkrCtxObj
10
+ from lkr.logger import logger
11
+
12
+ __all__ = ["group"]
13
+
14
+ mcp = FastMCP("lkr:codemode")
15
+ group = typer.Typer()
16
+
17
+
18
+
19
+ def get_mcp_sdk(ctx: LkrCtxObj):
20
+ sdk = get_auth(ctx).get_current_sdk(prompt_refresh_invalid_token=True)
21
+ sdk.auth.settings.agent_tag += "-codemode"
22
+ return sdk
23
+
24
+
25
+
26
+ def to_primitive(obj):
27
+ seen = set()
28
+
29
+ def _to_primitive(o):
30
+ if isinstance(o, (str, int, float, bool, type(None))):
31
+ return o
32
+
33
+ obj_id = id(o)
34
+ if obj_id in seen:
35
+ return f"<Circular reference to {type(o).__name__}>"
36
+ seen.add(obj_id)
37
+
38
+ try:
39
+ if isinstance(o, list):
40
+ return [_to_primitive(item) for item in o]
41
+ elif isinstance(o, dict):
42
+ return {k: _to_primitive(v) for k, v in o.items()}
43
+ else:
44
+ try:
45
+ return _to_primitive(vars(o))
46
+ except TypeError:
47
+ return str(o)
48
+ except Exception:
49
+ return str(o)
50
+ finally:
51
+ seen.remove(obj_id)
52
+
53
+ return _to_primitive(obj)
54
+
55
+
56
+
57
+ @mcp.tool()
58
+ def run_python_code(code: str) -> str:
59
+ """
60
+ Execute Python code safely with access to all Looker SDK methods as global functions.
61
+ Capture the result and any print outputs.
62
+ """
63
+ try:
64
+ ctx = LkrCtxObj(force_oauth=False)
65
+ sdk = get_mcp_sdk(ctx)
66
+
67
+ external_funcs = {}
68
+ for name, method in inspect.getmembers(sdk, predicate=inspect.ismethod):
69
+ if not name.startswith('_'):
70
+ # Wrap in a lambda to recursively convert output to primitives
71
+ def make_wrapper(m):
72
+ def wrapper(*args, **kwargs):
73
+ res = m(*args, **kwargs)
74
+ return to_primitive(res)
75
+ return wrapper
76
+ external_funcs[name] = make_wrapper(method)
77
+
78
+ m = pydantic_monty.Monty(code)
79
+ result = m.run(external_functions=external_funcs)
80
+
81
+ # Monty run returns a MontyComplete or None
82
+ output = str(getattr(result, "output", "")) if result is not None else ""
83
+
84
+ # Try to append captured stdout if available on the object
85
+ stdout = getattr(result, "stdout", None) if result is not None else None
86
+ if stdout:
87
+ return f"Output:\n{output}\nStdout:\n{stdout}"
88
+ return output
89
+ except Exception as e:
90
+ logger.error(f"Error executing Monty: {e}")
91
+ if is_auth_expired(e):
92
+ return "Error: Your Looker OAuth session has expired. Please run 'lkr auth login' to re-authenticate."
93
+ return f"Error: {str(e)}"
94
+
95
+
96
+ @group.command(name="run")
97
+ def run(
98
+ ctx: typer.Context,
99
+ debug: bool = typer.Option(False, help="Debug mode"),
100
+ ):
101
+ mcp.run()
102
+
103
+ if __name__ == "__main__":
104
+ mcp.run("sse")
@@ -42,6 +42,7 @@ def add_optional_typer_group(app, import_path, group_name, extra_message=None):
42
42
  add_optional_typer_group(app, "lkr.mcp.main.group", "mcp")
43
43
  add_optional_typer_group(app, "lkr.observability.main.group", "observability")
44
44
  add_optional_typer_group(app, "lkr.tools.main.group", "tools")
45
+ add_optional_typer_group(app, "lkr.codemode.main.group", "code-mode")
45
46
 
46
47
  @app.callback()
47
48
  def callback(