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.
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/PKG-INFO +1 -1
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/codemode.md +10 -1
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/codemode/main.py +44 -10
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/pyproject.toml +1 -1
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/tests/test_codemode.py +35 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/.github/workflows/ci.yml +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/.github/workflows/release.yml +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/.gitignore +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/.python-version +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/.vscode/launch.json +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/.vscode/settings.json +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/Dockerfile +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/LICENSE +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/Makefile +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/README.md +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/cloudbuild.yaml +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/__init__.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/auth/__init__.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/auth/main.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/auth/oauth.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/auth_service.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/classes.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/codemode/LOCAL.md +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/codemode/__init__.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/codemode/constant.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/codemode/download_swagger.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/codemode/examples.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/codemode/help.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/codemode/readme.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/codemode/swagger.json +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/codemode/type.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/constants.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/custom_types.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/exceptions.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/extended_sdk_methods/__init__.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/extended_sdk_methods/classes.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/extended_sdk_methods/main.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/logger.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/main.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/mcp/classes.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/mcp/main.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/mcp/utils.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/observability/classes.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/observability/embed_container.html +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/observability/main.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/observability/utils.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/tools/classes.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/tools/main.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr/tools/permission_deprecation.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/lkr.md +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/tests/TESTING.md +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/tests/test_dependency_resolution.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/tests/test_deps.sh +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/tests/test_extended_sdk_methods.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/tests/test_oauth_account.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/tests/test_permission_deprecation.py +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/ty.toml +0 -0
- {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.41}/uv.lock +0 -0
|
@@ -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
|
-
|
|
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
|
-
#
|
|
130
|
-
#
|
|
131
|
-
|
|
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 =
|
|
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
|
|
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,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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|