lkr-dev-cli 0.0.40__tar.gz → 0.0.41__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 (58) hide show
  1. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/PKG-INFO +1 -1
  2. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/codemode.md +10 -1
  3. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/codemode/main.py +44 -10
  4. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/pyproject.toml +1 -1
  5. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/tests/test_codemode.py +35 -0
  6. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/.github/workflows/ci.yml +0 -0
  7. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/.github/workflows/release.yml +0 -0
  8. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/.gitignore +0 -0
  9. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/.python-version +0 -0
  10. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/.vscode/launch.json +0 -0
  11. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/.vscode/settings.json +0 -0
  12. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/Dockerfile +0 -0
  13. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/LICENSE +0 -0
  14. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/Makefile +0 -0
  15. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/README.md +0 -0
  16. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/cloudbuild.yaml +0 -0
  17. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/__init__.py +0 -0
  18. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/auth/__init__.py +0 -0
  19. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/auth/main.py +0 -0
  20. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/auth/oauth.py +0 -0
  21. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/auth_service.py +0 -0
  22. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/classes.py +0 -0
  23. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/codemode/LOCAL.md +0 -0
  24. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/codemode/__init__.py +0 -0
  25. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/codemode/constant.py +0 -0
  26. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/codemode/download_swagger.py +0 -0
  27. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/codemode/examples.py +0 -0
  28. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/codemode/help.py +0 -0
  29. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/codemode/readme.py +0 -0
  30. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/codemode/swagger.json +0 -0
  31. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/codemode/type.py +0 -0
  32. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/constants.py +0 -0
  33. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/custom_types.py +0 -0
  34. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/exceptions.py +0 -0
  35. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/extended_sdk_methods/__init__.py +0 -0
  36. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/extended_sdk_methods/classes.py +0 -0
  37. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/extended_sdk_methods/main.py +0 -0
  38. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/logger.py +0 -0
  39. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/main.py +0 -0
  40. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/mcp/classes.py +0 -0
  41. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/mcp/main.py +0 -0
  42. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/mcp/utils.py +0 -0
  43. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/observability/classes.py +0 -0
  44. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/observability/embed_container.html +0 -0
  45. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/observability/main.py +0 -0
  46. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/observability/utils.py +0 -0
  47. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/tools/classes.py +0 -0
  48. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/tools/main.py +0 -0
  49. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/tools/permission_deprecation.py +0 -0
  50. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr.md +0 -0
  51. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/tests/TESTING.md +0 -0
  52. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/tests/test_dependency_resolution.py +0 -0
  53. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/tests/test_deps.sh +0 -0
  54. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/tests/test_extended_sdk_methods.py +0 -0
  55. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/tests/test_oauth_account.py +0 -0
  56. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/tests/test_permission_deprecation.py +0 -0
  57. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/ty.toml +0 -0
  58. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lkr-dev-cli
3
- Version: 0.0.40
3
+ Version: 0.0.41
4
4
  Summary: lkr: a command line interface for looker
5
5
  Author: bwebs
6
6
  License-Expression: MIT
@@ -47,5 +47,14 @@ To check things out on a web panel:
47
47
  npx @modelcontextprotocol/inspector uvx -q --from lkr-dev-cli[codemode] lkr code-mode run
48
48
  ```
49
49
 
50
- ```
50
+ ## Architectural & Troubleshooting Notes
51
+
52
+ ### The stdio Stream Corruption Trap
53
+ When running an MCP server over standard input/output (`stdio`) transport, JSON-RPC packets are transmitted directly through the process's `stdout`.
54
+
55
+ If Python code executed within the server (such as tool execution or third-party libraries) writes raw text directly to `sys.stdout` via standard `print()` calls, that raw text corrupts the JSON-RPC response stream. This causes fatal JSON parsing errors on the client (e.g., `invalid character 'C' looking for beginning of value`) and immediately closes the connection.
56
+
57
+ To safeguard against this:
58
+ 1. `run_python_code` automatically wraps all Monty execution in a `redirect_stdout` block.
59
+ 2. If any `print()` output is captured, it is safely packaged and returned as part of a valid JSON structure rather than polluting raw `stdout`.
51
60
 
@@ -1,7 +1,9 @@
1
1
  import inspect
2
- import io
3
2
  import json
4
- from contextlib import redirect_stdout
3
+ import os
4
+ import sys
5
+ import tempfile
6
+ from contextlib import contextmanager
5
7
 
6
8
  import typer
7
9
  import pydantic_monty
@@ -21,8 +23,38 @@ __all__ = ["group"]
21
23
  mcp = FastMCP("lkr:codemode")
22
24
  group = typer.Typer()
23
25
  ctx_lkr: LkrCtxObj | None = None
24
-
25
-
26
+ class OSCapture:
27
+ def __init__(self):
28
+ self.output = ""
29
+
30
+ @contextmanager
31
+ def capture_os_stdout():
32
+ cap = OSCapture()
33
+ fd, temp_path = tempfile.mkstemp()
34
+
35
+ if sys.stdout and hasattr(sys.stdout, "flush"):
36
+ sys.stdout.flush()
37
+
38
+ original_stdout = sys.stdout
39
+ original_stdout_fd = os.dup(1)
40
+ try:
41
+ os.dup2(fd, 1)
42
+ with os.fdopen(os.dup(1), "w") as f:
43
+ sys.stdout = f
44
+ yield cap
45
+ f.flush()
46
+ finally:
47
+ sys.stdout = original_stdout
48
+ os.dup2(original_stdout_fd, 1)
49
+ os.close(original_stdout_fd)
50
+ os.close(fd)
51
+
52
+ try:
53
+ with open(temp_path, "r") as f:
54
+ cap.output = f.read()
55
+ finally:
56
+ if os.path.exists(temp_path):
57
+ os.remove(temp_path)
26
58
 
27
59
  def get_mcp_sdk(ctx: LkrCtxObj):
28
60
  sdk = get_auth(ctx).get_current_sdk(prompt_refresh_invalid_token=True)
@@ -126,13 +158,12 @@ def run_python_code(code: str, dev_mode: bool = False) -> str:
126
158
 
127
159
  m = pydantic_monty.Monty(code)
128
160
 
129
- # Redirect stdout to capture any print() statements
130
- # and prevent them from corrupting the JSON-RPC stream
131
- f = io.StringIO()
132
- with redirect_stdout(f):
161
+ # Use low-level OS stdout capture to ensure any print() statements
162
+ # in Rust or sub-interpreters don't corrupt the JSON-RPC stream
163
+ with capture_os_stdout() as cap:
133
164
  result = m.run(external_functions=external_funcs)
134
165
 
135
- printed_output = f.getvalue()
166
+ printed_output = cap.output
136
167
 
137
168
  # m.run() returns the evaluated result of the last expression (which is already a primitive)
138
169
  try:
@@ -148,7 +179,10 @@ def run_python_code(code: str, dev_mode: bool = False) -> str:
148
179
  output = repr(result)
149
180
 
150
181
  if printed_output:
151
- return f"PRINTED OUTPUT:\n{printed_output}\nRESULT:\n{output}"
182
+ return json.dumps({
183
+ "stdout": printed_output,
184
+ "result": result
185
+ }, indent=2, default=str)
152
186
  return output
153
187
  except Exception as e:
154
188
  logger.error(f"Error executing Monty: {e}")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lkr-dev-cli"
3
- version = "0.0.40"
3
+ version = "0.0.41"
4
4
  description = "lkr: a command line interface for looker"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,4 +1,5 @@
1
1
  from typing import Any, cast
2
+ import json
2
3
  import pytest
3
4
  from unittest.mock import patch, MagicMock
4
5
  from looker_sdk.rtl.auth_session import AuthSession
@@ -247,4 +248,38 @@ return lookup('ProjectCommitRequest')
247
248
  assert "Looker automatically stages and commits" in result_model
248
249
 
249
250
 
251
+ def test_captured_print_json():
252
+ code = """
253
+ print("Hello from stdout")
254
+ return {"status": "success"}
255
+ """
256
+ result_raw = run_python_code(code)
257
+ data = json.loads(result_raw)
258
+ assert data["stdout"] == "Hello from stdout\n"
259
+ assert data["result"] == {"status": "success"}
260
+
261
+ def test_captured_print_json_many():
262
+ code = """
263
+ for i in range(10):
264
+ print(i)
265
+ return {"status": "success"}
266
+ """
267
+ result_raw = run_python_code(code)
268
+ data = json.loads(result_raw)
269
+ assert data["stdout"] == "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n"
270
+ assert data["result"] == {"status": "success"}
250
271
 
272
+
273
+ def test_parent_runtime_not_polluted(capfd):
274
+ code = """
275
+ print("This should be trapped and not leak to parent stdout")
276
+ print("Also checking second print")
277
+ return {"status": "clean"}
278
+ """
279
+ result_raw = run_python_code(code)
280
+ data = json.loads(result_raw)
281
+ assert "This should be trapped" in data["stdout"]
282
+ assert "Also checking" in data["stdout"]
283
+
284
+ out, err = capfd.readouterr()
285
+ assert out == ""
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes