lkr-dev-cli 0.0.34__tar.gz → 0.0.36__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 (47) hide show
  1. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/Makefile +3 -3
  2. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/PKG-INFO +3 -3
  3. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/README.md +2 -2
  4. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/codemode.md +6 -6
  5. lkr_dev_cli-0.0.36/lkr/codemode/LOCAL.md +56 -0
  6. lkr_dev_cli-0.0.36/lkr/codemode/main.py +200 -0
  7. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/lkr/logger.py +1 -1
  8. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/lkr/mcp/main.py +2 -0
  9. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/pyproject.toml +1 -1
  10. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/tests/test_codemode2.py +2 -0
  11. lkr_dev_cli-0.0.34/lkr/codemode/main.py +0 -104
  12. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/.github/workflows/release.yml +0 -0
  13. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/.github/workflows/test-dependencies.yml +0 -0
  14. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/.gitignore +0 -0
  15. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/.python-version +0 -0
  16. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/.vscode/launch.json +0 -0
  17. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/.vscode/settings.json +0 -0
  18. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/Dockerfile +0 -0
  19. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/LICENSE +0 -0
  20. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/cloudbuild.yaml +0 -0
  21. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/lkr/__init__.py +0 -0
  22. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/lkr/auth/__init__.py +0 -0
  23. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/lkr/auth/main.py +0 -0
  24. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/lkr/auth/oauth.py +0 -0
  25. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/lkr/auth_service.py +0 -0
  26. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/lkr/classes.py +0 -0
  27. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/lkr/codemode/__init__.py +0 -0
  28. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/lkr/constants.py +0 -0
  29. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/lkr/custom_types.py +0 -0
  30. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/lkr/exceptions.py +0 -0
  31. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/lkr/main.py +0 -0
  32. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/lkr/mcp/classes.py +0 -0
  33. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/lkr/mcp/utils.py +0 -0
  34. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/lkr/observability/classes.py +0 -0
  35. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/lkr/observability/embed_container.html +0 -0
  36. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/lkr/observability/main.py +0 -0
  37. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/lkr/observability/utils.py +0 -0
  38. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/lkr/tools/classes.py +0 -0
  39. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/lkr/tools/main.py +0 -0
  40. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/lkr/tools/permission_deprecation.py +0 -0
  41. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/lkr.md +0 -0
  42. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/tests/TESTING.md +0 -0
  43. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/tests/test_codemode.py +0 -0
  44. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/tests/test_dependency_resolution.py +0 -0
  45. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/tests/test_deps.sh +0 -0
  46. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/tests/test_permission_deprecation.py +0 -0
  47. {lkr_dev_cli-0.0.34 → lkr_dev_cli-0.0.36}/uv.lock +0 -0
@@ -17,10 +17,10 @@ codemode-start:
17
17
  @echo "Add this to your mcpServers config:"
18
18
  @echo "{"
19
19
  @echo " \"mcpServers\": {"
20
- @echo " \"lkr-codemode\": {"
20
+ @echo " \"lkr_codemode\": {"
21
21
  @echo " \"command\": \"uvx\","
22
- @echo " \"args\": [\"--from\", \"lkr-dev-cli[codemode]\", \"lkr\", \"code-mode\", \"run\"]"
22
+ @echo " \"args\": [\"-q\", \"--from\", \"lkr-dev-cli[codemode]\", \"lkr\", \"code-mode\", \"run\"]"
23
23
  @echo " }"
24
24
  @echo " }"
25
25
  @echo "}"
26
- uv run lkr code-mode run
26
+ uv run -q lkr code-mode run
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lkr-dev-cli
3
- Version: 0.0.34
3
+ Version: 0.0.36
4
4
  Summary: lkr: a command line interface for looker
5
5
  Author: bwebs
6
6
  License-Expression: MIT
@@ -109,9 +109,9 @@ Go to the Looker API Explorer for Register OAuth App (https://your.looker.instan
109
109
  - This only needs to be done once per instance
110
110
 
111
111
 
112
- ## Code-Mode
112
+ ## Code Mode
113
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).
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
115
 
116
116
  ## MCP
117
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
@@ -73,9 +73,9 @@ Go to the Looker API Explorer for Register OAuth App (https://your.looker.instan
73
73
  - This only needs to be done once per instance
74
74
 
75
75
 
76
- ## Code-Mode
76
+ ## Code Mode
77
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).
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
79
 
80
80
  ## MCP
81
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
@@ -1,4 +1,4 @@
1
- # Looker Code-Mode MCP Server
1
+ # Looker Code Mode MCP Server
2
2
 
3
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
4
 
@@ -11,7 +11,7 @@ The tool instantiates Looker SDK natively, searches all bound methods, and passe
11
11
  ### Key Features:
12
12
  - **100% Tool Coverage:** Accesses all Looker SDK public operations smoothly without token limits.
13
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.
14
+ - **Automatic PKCE Restarter:** Caught an invalid token? Code Mode immediately catches `InvalidRefreshTokenError` and safely opens up your PKCE authentication browser automatically.
15
15
  - **Extremely Secure:** Monty interpreter ensures isolated sandbox processing. No local filesystem accesses are exposed.
16
16
 
17
17
  ## Continuous Usage
@@ -19,7 +19,7 @@ The tool instantiates Looker SDK natively, searches all bound methods, and passe
19
19
  ### 1. Starting the Server
20
20
  To immediately trigger the stdio listener, use:
21
21
  ```bash
22
- uvx --from lkr-dev-cli[codemode] lkr code-mode run
22
+ uvx -q --from lkr-dev-cli[codemode] lkr code-mode run
23
23
  ```
24
24
 
25
25
  ### 2. Client Configuration
@@ -28,9 +28,9 @@ To hook this server into Cursor or Claude Desktop natively over stdio, append th
28
28
  ```json
29
29
  {
30
30
  "mcpServers": {
31
- "looker-codemode": {
31
+ "lkr_codemode": {
32
32
  "command": "uvx",
33
- "args": ["--from", "lkr-dev-cli[codemode]", "lkr", "code-mode", "run"],
33
+ "args": ["-q", "--from", "lkr-dev-cli[codemode]", "lkr", "code-mode", "run"],
34
34
  "env": {
35
35
  "LOOKERSDK_BASE_URL": "https://your.looker.instance",
36
36
  "LOOKERSDK_CLIENT_ID": "your-client-id",
@@ -44,7 +44,7 @@ To hook this server into Cursor or Claude Desktop natively over stdio, append th
44
44
  ### 3. Visual Inspector
45
45
  To check things out on a web panel:
46
46
  ```bash
47
- npx @modelcontextprotocol/inspector uvx --from lkr-dev-cli[codemode] lkr code-mode run
47
+ npx @modelcontextprotocol/inspector uvx -q --from lkr-dev-cli[codemode] lkr code-mode run
48
48
  ```
49
49
 
50
50
  ```
@@ -0,0 +1,56 @@
1
+ # Local Development Configuration for lkr:codemode
2
+
3
+ To run this MCP server locally from the repository (for development or testing), you can configure your MCP client (like Claude Desktop or Cursor) to use `uv run` with the `--directory` flag pointing to this project.
4
+
5
+ ### Claude Desktop Configuration
6
+
7
+ Add the following to your `claude_desktop_config.json` (usually located at `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
8
+
9
+ ```json
10
+ {
11
+ "mcpServers": {
12
+ "lkr_codemode_dev": {
13
+ "command": "/opt/homebrew/bin/uv",
14
+ "args": [
15
+ "-q",
16
+ "run",
17
+ "--directory",
18
+ "/path/to/lkr/cli",
19
+ "lkr",
20
+ "code-mode",
21
+ "run"
22
+ ],
23
+ "env": {
24
+ "LOOKERSDK_BASE_URL": "https://your-instance.cloud.looker.com",
25
+ "LOOKERSDK_CLIENT_ID": "your_client_id",
26
+ "LOOKERSDK_CLIENT_SECRET": "your_client_secret"
27
+ }
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ > [!WARNING]
34
+ > Do NOT place the directory path directly after `run` without the `--directory` flag (e.g., `["run", "/path/to/project", ...]`). `uv` will attempt to execute the directory as a command, resulting in a `Permission denied (os error 13)` error.
35
+
36
+ > [!TIP]
37
+ > **macOS GUI Apps & PATH:** Claude Desktop and Cursor do not inherit your terminal's `PATH` environment variable. If you simply use `"uv"` as the command, the app may fail to start the server because it cannot find `uv`. Using the absolute path (e.g., `/opt/homebrew/bin/uv` or `~/.cargo/bin/uv`) ensures the IDE can launch the process. Find out your path by running `which uv` in your terminal.
38
+
39
+ ### Cursor Integration
40
+
41
+ To add this local dev server to Cursor:
42
+
43
+ 1. Open **Settings** (`Cmd + ,`).
44
+ 2. Go to **Features** -> **MCP**.
45
+ 3. Click **+ Add New MCP Server**.
46
+ 4. **Name**: `Looker Codemode (Local)`
47
+ 5. **Type**: `command`
48
+ 6. **Command**: `uv run -q --directory /Users/bryanweber/projects/lkr/cli lkr code-mode run`
49
+
50
+ ### Testing Interactively via MCP Inspector
51
+
52
+ You can test the server and its tools interactively in your browser by running the MCP inspector from your terminal:
53
+
54
+ ```bash
55
+ npx @modelcontextprotocol/inspector uv run -q --directory /Users/bryanweber/projects/lkr/cli lkr code-mode run
56
+ ```
@@ -0,0 +1,200 @@
1
+ import inspect
2
+ import io
3
+ import json
4
+ from contextlib import redirect_stdout
5
+ from typing import Optional
6
+
7
+ import typer
8
+ import pydantic_monty
9
+ from mcp.server.fastmcp import FastMCP
10
+
11
+ from lkr.auth_service import get_auth, is_auth_expired
12
+ from lkr.classes import LkrCtxObj
13
+ from lkr.logger import logger
14
+
15
+ __all__ = ["group"]
16
+
17
+ mcp = FastMCP("lkr:codemode")
18
+ group = typer.Typer()
19
+
20
+
21
+
22
+ def get_mcp_sdk(ctx: LkrCtxObj):
23
+ sdk = get_auth(ctx).get_current_sdk(prompt_refresh_invalid_token=True)
24
+ sdk.auth.settings.agent_tag += "-codemode"
25
+ return sdk
26
+
27
+
28
+
29
+ def to_primitive(obj):
30
+ seen = set()
31
+
32
+ def _to_primitive(o):
33
+ if isinstance(o, (str, int, float, bool, type(None))):
34
+ return o
35
+
36
+ obj_id = id(o)
37
+ if obj_id in seen:
38
+ return f"<Circular reference to {type(o).__name__}>"
39
+ seen.add(obj_id)
40
+
41
+ try:
42
+ if isinstance(o, list):
43
+ return [_to_primitive(item) for item in o]
44
+ elif isinstance(o, dict):
45
+ return {k: _to_primitive(v) for k, v in o.items()}
46
+ else:
47
+ try:
48
+ return _to_primitive(vars(o))
49
+ except TypeError:
50
+ return str(o)
51
+ except Exception:
52
+ return str(o)
53
+ finally:
54
+ seen.remove(obj_id)
55
+
56
+ return _to_primitive(obj)
57
+
58
+
59
+
60
+ @mcp.tool()
61
+ def run_python_code(code: str) -> str:
62
+ """
63
+ Execute Python code safely with access to all Looker SDK methods as global functions.
64
+ Capture the result.
65
+
66
+ AGENT HINTS:
67
+ - Use `dir()` and `help('method_name')` to discover available SDK methods.
68
+ - Do not instantiate an SDK; use global functions directly (e.g. `me()`).
69
+ - Returned Looker models are primitive dictionaries (use `obj["id"]`, not `obj.id`).
70
+ - Return your output (avoid using print() as it may pollute the stdio stream).
71
+ - Recursion: Use `folder_children(id)` to traverse nested folders.
72
+ """
73
+ try:
74
+ ctx = LkrCtxObj(force_oauth=False)
75
+ sdk = get_mcp_sdk(ctx)
76
+
77
+ external_funcs = {}
78
+ for name, method in inspect.getmembers(sdk, predicate=inspect.ismethod):
79
+ if not name.startswith('_'):
80
+ # Wrap in a lambda to recursively convert output to primitives
81
+ def make_wrapper(m):
82
+ def wrapper(*args, **kwargs):
83
+ res = m(*args, **kwargs)
84
+ return to_primitive(res)
85
+ return wrapper
86
+ external_funcs[name] = make_wrapper(method)
87
+
88
+ # Provide helper functions for the LLM to explore the SDK
89
+ external_funcs['dir'] = lambda: list(external_funcs.keys())
90
+
91
+ def _help(name: str) -> str:
92
+ if name in external_funcs:
93
+ if hasattr(sdk, name):
94
+ return getattr(sdk, name).__doc__ or "No docstring available."
95
+ return f"{name} is a built-in helper function."
96
+ return f"Function '{name}' not found."
97
+ external_funcs['help'] = _help
98
+
99
+ m = pydantic_monty.Monty(code)
100
+
101
+ # Redirect stdout to capture any print() statements
102
+ # and prevent them from corrupting the JSON-RPC stream
103
+ f = io.StringIO()
104
+ with redirect_stdout(f):
105
+ result = m.run(external_functions=external_funcs)
106
+
107
+ printed_output = f.getvalue()
108
+
109
+ # m.run() returns the evaluated result of the last expression (which is already a primitive)
110
+ try:
111
+ # Use JSON for nice formatting if it's a dict/list
112
+ if result is not None:
113
+ if isinstance(result, str):
114
+ output = result
115
+ else:
116
+ output = json.dumps(result, indent=2, default=str)
117
+ else:
118
+ output = ""
119
+ except Exception:
120
+ output = repr(result)
121
+
122
+ if printed_output:
123
+ return f"PRINTED OUTPUT:\n{printed_output}\nRESULT:\n{output}"
124
+ return output
125
+ except Exception as e:
126
+ logger.error(f"Error executing Monty: {e}")
127
+ if is_auth_expired(e):
128
+ return "Error: Your Looker OAuth session has expired. Please run 'lkr auth login' to re-authenticate."
129
+ return f"Error: {str(e)}"
130
+
131
+
132
+ @mcp.resource("looker://agent-hints")
133
+ def get_agent_hints() -> str:
134
+ """Crucial hints and rules for AI agents writing Python for the Looker SDK."""
135
+ return """
136
+ 1. **Global Functions**: All Looker SDK methods are global. Use `me()`, not `sdk.me()`.
137
+ 2. **Dict Access**: Return values are dictionaries, not objects. Use `user["id"]`, not `user.id`.
138
+ 3. **Discovery**: Use `dir()` and `help('method')` to explore the SDK.
139
+ 4. **No Imports**: Do not `import looker_sdk`.
140
+ 5. **Output**: Return your results instead of using `print()`.
141
+ 6. **Efficiency**: Always use the `fields` parameter (e.g., `all_dashboards(fields="id,title")`) when listing many objects to prevent timeouts.
142
+ 7. **Nested Folders**: Use `folder_children(id)` to get sub-folders.
143
+ """
144
+
145
+
146
+ @mcp.prompt("explore_looker_sdk")
147
+ def explore_looker_sdk() -> str:
148
+ """Provide examples for how to explore the Looker SDK in code mode."""
149
+ return '''
150
+ To explore the Looker SDK, you can use the injected `dir()` and `help()` helpers.
151
+ Do not use print() as it may corrupt the MCP output stream; always return the result.
152
+
153
+ Example 1: Find all dashboard-related methods
154
+ ```python
155
+ return [m for m in dir() if 'dashboard' in m.lower()]
156
+ ```
157
+
158
+ Example 2: Get the description of a specific method
159
+ ```python
160
+ return help('search_dashboards')
161
+ ```
162
+ '''
163
+
164
+
165
+ @mcp.prompt("list_personal_dashboards")
166
+ def list_personal_dashboards() -> str:
167
+ """Provide an example of how to recursively list dashboards in a user's personal folder."""
168
+ return '''
169
+ Here is a robust example of how to traverse the folder hierarchy using the Looker SDK in code mode:
170
+
171
+ ```python
172
+ def get_all_items(folder_id):
173
+ f = folder(folder_id)
174
+ items = {
175
+ "dashboards": f.get("dashboards", []),
176
+ "looks": f.get("looks", [])
177
+ }
178
+
179
+ for child in folder_children(folder_id):
180
+ child_items = get_all_items(child["id"])
181
+ items["dashboards"].extend(child_items["dashboards"])
182
+ items["looks"].extend(child_items["looks"])
183
+
184
+ return items
185
+
186
+ me_data = me()
187
+ return get_all_items(me_data["personal_folder_id"])
188
+ ```
189
+ '''
190
+
191
+
192
+ @group.command(name="run")
193
+ def run(
194
+ ctx: typer.Context,
195
+ debug: bool = typer.Option(False, help="Debug mode"),
196
+ ):
197
+ mcp.run()
198
+
199
+ if __name__ == "__main__":
200
+ mcp.run("sse")
@@ -36,7 +36,7 @@ theme = Theme(
36
36
  ) if RICH_AVAILABLE else None
37
37
 
38
38
  # Create a console for logging
39
- console = Console(theme=theme) if RICH_AVAILABLE else None
39
+ console = Console(theme=theme, stderr=True) if RICH_AVAILABLE else None
40
40
 
41
41
  # Configure the logging handler
42
42
  handler = RichHandler(
@@ -1,4 +1,5 @@
1
1
  # server.py
2
+ import sys
2
3
  import threading
3
4
  from datetime import datetime
4
5
  from pathlib import Path
@@ -549,6 +550,7 @@ def run(
549
550
  set_log_level(LogLevel.DEBUG)
550
551
  else:
551
552
  set_log_level(LogLevel.ERROR)
553
+ sys.stdout = sys.stderr
552
554
  mcp.run()
553
555
 
554
556
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lkr-dev-cli"
3
- version = "0.0.34"
3
+ version = "0.0.36"
4
4
  description = "lkr: a command line interface for looker"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,5 +1,7 @@
1
1
  from lkr.codemode.main import run_python_code
2
2
 
3
+ # Find me all my dashboards and looks within all my personal folder and nested folders
4
+
3
5
  code = """
4
6
  me_obj = me()
5
7
  personal_folder_id = me_obj["personal_folder_id"]
@@ -1,104 +0,0 @@
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")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes