lcode-cli 0.10.0__tar.gz → 0.11.0__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 (113) hide show
  1. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/CHANGELOG.md +17 -1
  2. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/PKG-INFO +8 -4
  3. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/README.md +5 -3
  4. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/docs/how-it-works.md +1 -0
  5. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/docs/mcp.md +49 -1
  6. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/docs/quickstart.md +1 -1
  7. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/pyproject.toml +5 -0
  8. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/__init__.py +1 -1
  9. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/mcp/catalog.py +4 -0
  10. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/mcp/catalog.toml +57 -1
  11. lcode_cli-0.11.0/src/lcode/mcp/server.py +152 -0
  12. lcode_cli-0.11.0/src/lcode/servers/__init__.py +1 -0
  13. lcode_cli-0.11.0/src/lcode/servers/encord.py +285 -0
  14. lcode_cli-0.11.0/src/lcode/servers/valohai.py +329 -0
  15. lcode_cli-0.11.0/tests/test_servers.py +379 -0
  16. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/uv.lock +452 -2
  17. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/.editorconfig +0 -0
  18. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/.github/CODEOWNERS +0 -0
  19. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  20. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  21. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  22. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
  23. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  24. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/.github/dependabot.yml +0 -0
  25. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/.github/workflows/ci.yml +0 -0
  26. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/.github/workflows/docs.yml +0 -0
  27. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/.github/workflows/release.yml +0 -0
  28. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/.gitignore +0 -0
  29. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/.pre-commit-config.yaml +0 -0
  30. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/AGENTS.md +0 -0
  31. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/CODE_OF_CONDUCT.md +0 -0
  32. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/CONTRIBUTING.md +0 -0
  33. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/LICENSE +0 -0
  34. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/SECURITY.md +0 -0
  35. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/docs/agents.md +0 -0
  36. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/docs/assets/demo.svg +0 -0
  37. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/docs/assets/extra.css +0 -0
  38. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/docs/assets/logo.svg +0 -0
  39. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/docs/assets/social-preview.png +0 -0
  40. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/docs/bench.md +0 -0
  41. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/docs/changelog.md +0 -0
  42. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/docs/commands.md +0 -0
  43. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/docs/configuration.md +0 -0
  44. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/docs/contributing.md +0 -0
  45. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/docs/index.md +0 -0
  46. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/docs/installation.md +0 -0
  47. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/docs/memory.md +0 -0
  48. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/docs/models.md +0 -0
  49. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/docs/sandbox.md +0 -0
  50. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/docs/troubleshooting.md +0 -0
  51. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/docs/usage.md +0 -0
  52. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/examples/README.md +0 -0
  53. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/examples/commands/review.md +0 -0
  54. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/examples/skills/commit-message/SKILL.md +0 -0
  55. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/examples/skills/commit-message/scripts/staged.sh +0 -0
  56. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/install.sh +0 -0
  57. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/mkdocs.yml +0 -0
  58. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/overrides/main.html +0 -0
  59. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/scripts/record_demo.py +0 -0
  60. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/scripts/social-preview.html +0 -0
  61. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/__main__.py +0 -0
  62. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/agent.py +0 -0
  63. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/bench.py +0 -0
  64. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/catalog.py +0 -0
  65. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/checkpoints.py +0 -0
  66. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/cli.py +0 -0
  67. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/config.py +0 -0
  68. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/extensions.py +0 -0
  69. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/frontmatter.py +0 -0
  70. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/hardware.py +0 -0
  71. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/limits.py +0 -0
  72. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/mcp/__init__.py +0 -0
  73. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/mcp/auth.py +0 -0
  74. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/mcp/client.py +0 -0
  75. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/mcp/commands.py +0 -0
  76. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/mcp/config.py +0 -0
  77. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/mcp/http.py +0 -0
  78. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/mcp/manager.py +0 -0
  79. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/mcp/protocol.py +0 -0
  80. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/mcp/stdio.py +0 -0
  81. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/memory.py +0 -0
  82. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/models.toml +0 -0
  83. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/ollama.py +0 -0
  84. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/permissions.py +0 -0
  85. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/planning.py +0 -0
  86. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/render.py +0 -0
  87. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/repl.py +0 -0
  88. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/sandbox.py +0 -0
  89. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/sessions.py +0 -0
  90. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/subagents.py +0 -0
  91. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/tools.py +0 -0
  92. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/vision.py +0 -0
  93. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/src/lcode/web.py +0 -0
  94. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/tests/conftest.py +0 -0
  95. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/tests/fake_mcp_server.py +0 -0
  96. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/tests/test_agent.py +0 -0
  97. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/tests/test_bench.py +0 -0
  98. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/tests/test_catalog.py +0 -0
  99. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/tests/test_checkpoints.py +0 -0
  100. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/tests/test_cli.py +0 -0
  101. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/tests/test_config.py +0 -0
  102. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/tests/test_context_picker.py +0 -0
  103. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/tests/test_extensions.py +0 -0
  104. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/tests/test_mcp.py +0 -0
  105. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/tests/test_memory.py +0 -0
  106. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/tests/test_permissions.py +0 -0
  107. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/tests/test_plan_mode.py +0 -0
  108. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/tests/test_sandbox.py +0 -0
  109. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/tests/test_sessions.py +0 -0
  110. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/tests/test_subagents.py +0 -0
  111. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/tests/test_tools.py +0 -0
  112. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/tests/test_vision.py +0 -0
  113. {lcode_cli-0.10.0 → lcode_cli-0.11.0}/tests/test_web.py +0 -0
@@ -6,6 +6,21 @@ All notable changes to lcode are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.11.0] - 2026-10-05
10
+
11
+ ### Added
12
+
13
+ - Ready-made MCP servers for data and ML platforms:
14
+ - `lcode mcp add metabase` connects to the MCP server built into Metabase 60 and later, with browser
15
+ sign-in, so the model sees your data with your Metabase permissions.
16
+ - `lcode mcp add encord` and `lcode mcp add valohai`: those platforms have no MCP server of their own,
17
+ so lcode now ships `lcode-mcp-encord` (projects, datasets, ontologies, labeling progress, tasks and
18
+ labels, built on the Encord SDK) and `lcode-mcp-valohai` (executions with their logs, metrics and
19
+ outputs, pipelines). Both are read-only unless started with `--allow-writes`, which adds
20
+ assigning tasks and setting priorities (Encord) or starting and stopping executions (Valohai), and
21
+ both work with other MCP clients too. See [MCP servers](https://nasser1941.github.io/lcode/mcp/#encord).
22
+ - Catalog questions that ask for an address now accept it without `https://`.
23
+
9
24
  ## [0.10.0] - 2026-10-05
10
25
 
11
26
  ### Added
@@ -287,7 +302,8 @@ First public release.
287
302
  - `AGENTS.md` project instructions and `/init` to generate them.
288
303
  - One-line installer for Linux and macOS.
289
304
 
290
- [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.10.0...HEAD
305
+ [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.11.0...HEAD
306
+ [0.11.0]: https://github.com/nasser1941/lcode/compare/v0.10.0...v0.11.0
291
307
  [0.10.0]: https://github.com/nasser1941/lcode/compare/v0.9.0...v0.10.0
292
308
  [0.9.0]: https://github.com/nasser1941/lcode/compare/v0.8.0...v0.9.0
293
309
  [0.8.0]: https://github.com/nasser1941/lcode/compare/v0.7.0...v0.8.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lcode-cli
3
- Version: 0.10.0
3
+ Version: 0.11.0
4
4
  Summary: A local-first terminal coding agent powered by open-weight models on your own GPU or Mac, via Ollama.
5
5
  Project-URL: Homepage, https://nasser1941.github.io/lcode/
6
6
  Project-URL: Documentation, https://nasser1941.github.io/lcode/
@@ -30,6 +30,8 @@ Requires-Dist: prompt-toolkit>=3.0.43
30
30
  Requires-Dist: requests>=2.31
31
31
  Requires-Dist: rich>=13.7
32
32
  Requires-Dist: tomli>=2.0; python_version < '3.11'
33
+ Provides-Extra: encord
34
+ Requires-Dist: encord>=0.1.200; extra == 'encord'
33
35
  Description-Content-Type: text/markdown
34
36
 
35
37
  <p align="center">
@@ -95,7 +97,7 @@ through [Ollama](https://ollama.com), and when it needs current information it c
95
97
  - **Bring your own model.** Eight curated open-weight models, all tested end to end, or any Ollama model
96
98
  with tool calling.
97
99
  - **MCP servers, ready to go.** Connect Jira and Confluence, GitHub, AWS, Google Drive, Grafana,
98
- Google Cloud, Sentry, Linear, Notion, Postgres, Kubernetes and more with one command
100
+ Google Cloud, Sentry, Linear, Notion, Postgres, Kubernetes, Metabase, Encord, Valohai and more with one command
99
101
  (`lcode mcp add atlassian`), image generation with ComfyUI, or any other MCP server. Browser sign-in (OAuth) is built in.
100
102
  - **Sees images.** Attach a screenshot or mockup with `@path` and lcode looks at it, using the
101
103
  vision part of your model. With ComfyUI (`lcode mcp add comfyui`) it can also generate and edit
@@ -199,8 +201,10 @@ fills up. Details: [How it works](https://nasser1941.github.io/lcode/how-it-work
199
201
 
200
202
  See the pinned [Roadmap issue](https://github.com/nasser1941/lcode/issues/10). Next up:
201
203
 
202
- - [Ready-made MCP for Metabase, Encord and Valohai](https://github.com/nasser1941/lcode/issues/53)
203
- - Then: [smarter context for small windows](https://github.com/nasser1941/lcode/issues/46), [language servers](https://github.com/nasser1941/lcode/issues/44), [a repository map with local embeddings](https://github.com/nasser1941/lcode/issues/45), [hooks](https://github.com/nasser1941/lcode/issues/43)
204
+ - [Smarter context for small windows](https://github.com/nasser1941/lcode/issues/46)
205
+ - [Code intelligence with language servers](https://github.com/nasser1941/lcode/issues/44)
206
+ - [A repository map and semantic search with local embeddings](https://github.com/nasser1941/lcode/issues/45)
207
+ - [Hooks and permission rules](https://github.com/nasser1941/lcode/issues/43)
204
208
  - More models in the catalog, and test reports from your hardware ([report one](https://github.com/nasser1941/lcode/issues/new?template=model_request.yml))
205
209
 
206
210
  ## Contributing
@@ -61,7 +61,7 @@ through [Ollama](https://ollama.com), and when it needs current information it c
61
61
  - **Bring your own model.** Eight curated open-weight models, all tested end to end, or any Ollama model
62
62
  with tool calling.
63
63
  - **MCP servers, ready to go.** Connect Jira and Confluence, GitHub, AWS, Google Drive, Grafana,
64
- Google Cloud, Sentry, Linear, Notion, Postgres, Kubernetes and more with one command
64
+ Google Cloud, Sentry, Linear, Notion, Postgres, Kubernetes, Metabase, Encord, Valohai and more with one command
65
65
  (`lcode mcp add atlassian`), image generation with ComfyUI, or any other MCP server. Browser sign-in (OAuth) is built in.
66
66
  - **Sees images.** Attach a screenshot or mockup with `@path` and lcode looks at it, using the
67
67
  vision part of your model. With ComfyUI (`lcode mcp add comfyui`) it can also generate and edit
@@ -165,8 +165,10 @@ fills up. Details: [How it works](https://nasser1941.github.io/lcode/how-it-work
165
165
 
166
166
  See the pinned [Roadmap issue](https://github.com/nasser1941/lcode/issues/10). Next up:
167
167
 
168
- - [Ready-made MCP for Metabase, Encord and Valohai](https://github.com/nasser1941/lcode/issues/53)
169
- - Then: [smarter context for small windows](https://github.com/nasser1941/lcode/issues/46), [language servers](https://github.com/nasser1941/lcode/issues/44), [a repository map with local embeddings](https://github.com/nasser1941/lcode/issues/45), [hooks](https://github.com/nasser1941/lcode/issues/43)
168
+ - [Smarter context for small windows](https://github.com/nasser1941/lcode/issues/46)
169
+ - [Code intelligence with language servers](https://github.com/nasser1941/lcode/issues/44)
170
+ - [A repository map and semantic search with local embeddings](https://github.com/nasser1941/lcode/issues/45)
171
+ - [Hooks and permission rules](https://github.com/nasser1941/lcode/issues/43)
170
172
  - More models in the catalog, and test reports from your hardware ([report one](https://github.com/nasser1941/lcode/issues/new?template=model_request.yml))
171
173
 
172
174
  ## Contributing
@@ -85,6 +85,7 @@ the Ollama server you configure, it only contacts the web when the model searche
85
85
  | `agent.py` | System prompt, streaming, tool loop, compaction, sessions |
86
86
  | `tools.py` | Tool schemas and implementations |
87
87
  | `permissions.py` | Approval prompts and the read-only allowlist |
88
+ | `servers/` | The MCP servers lcode ships for services without one: Encord, Valohai |
88
89
  | `mcp/` | MCP client: stdio and HTTP transports, OAuth sign-in, server settings, the catalog, `lcode mcp` |
89
90
  | `bench.py` | `lcode bench`: the benchmark tasks, their checks and the reports |
90
91
  | `vision.py` | Looking at images with a model that can see |
@@ -35,13 +35,16 @@ and find the code for the top one."*
35
35
  | `kubernetes` | Pods, deployments, logs and events, read-only | `npx` and a kubeconfig |
36
36
  | `linear` | Issues, projects and cycles | a Linear account (browser sign-in) |
37
37
  | `notion` | Pages and databases | a Notion account (browser sign-in) |
38
+ | `metabase` | Explore your data and ask questions through Metabase's semantic layer | Metabase 60 or later with MCP on ([below](#metabase)) |
39
+ | `encord` | Projects, datasets, ontologies, labeling progress, tasks and labels, read-only | `uvx`, an SSH key registered in Encord ([below](#encord)) |
40
+ | `valohai` | ML executions with their logs, metrics and outputs, and pipelines, read-only | `uvx`, a Valohai API token ([below](#valohai)) |
38
41
 
39
42
  `npx` comes with [Node.js](https://nodejs.org) (18 or newer) and `uvx` with
40
43
  [uv](https://docs.astral.sh/uv/getting-started/installation/). `lcode mcp catalog` shows what's missing
41
44
  on your machine.
42
45
 
43
46
  Servers that can change things start read-only where the server supports it (`aws`, `postgres`,
44
- `kubernetes`); the setup note tells you which setting to change to allow writes.
47
+ `kubernetes`, `encord`, `valohai`); the setup note tells you which setting to change to allow writes.
45
48
 
46
49
  ### Signing in
47
50
 
@@ -134,6 +137,51 @@ size, prompt), which can take several minutes of trial and error. Once an image
134
137
  lcode to save that workflow in your project (for example `assets/icon.workflow.json`) and reuse it:
135
138
  later images are a single `run_workflow` call.
136
139
 
140
+ ### Metabase
141
+
142
+ Metabase has its own MCP server inside every instance (Metabase 60 and later), at
143
+ `/api/metabase-mcp`. An admin switches it on under **Admin > AI > MCP**. Then:
144
+
145
+ ```bash
146
+ lcode mcp add metabase # asks for your Metabase address, then signs in in the browser
147
+ ```
148
+
149
+ You sign in with your Metabase account, and the model sees exactly the data your Metabase
150
+ permissions allow. See [Metabase's MCP docs](https://www.metabase.com/docs/latest/ai/mcp).
151
+
152
+ ### Encord
153
+
154
+ Encord has no MCP server of its own, so lcode ships one: `lcode-mcp-encord`, built on the Encord SDK
155
+ and run with `uvx --from "lcode-cli[encord]"`. It signs in with an SSH key that you register in
156
+ Encord under **Settings > Public keys**; `lcode mcp add encord` asks for the key file's path.
157
+
158
+ | Tool | |
159
+ |---|---|
160
+ | `list_projects`, `list_datasets` | What you can see |
161
+ | `get_project`, `get_ontology` | A project's datasets, ontology classes and workflow stages |
162
+ | `workflow_progress` | How many tasks are in each workflow stage, by status |
163
+ | `list_tasks` | Data units with their stage, status and last edit, filtered by stage or title |
164
+ | `get_labels` | The labels on one data unit: counts per class and the label JSON |
165
+ | `assign_task`, `set_priority` | Only with `--allow-writes` in the server's `args` in `mcp.json` |
166
+
167
+ ### Valohai
168
+
169
+ Valohai has no MCP server of its own either, so lcode ships `lcode-mcp-valohai`, which talks to
170
+ Valohai's REST API. It needs an API token (**My Profile > Authentication > Manage tokens**), and the
171
+ address of a self-hosted installation if you use one; `lcode mcp add valohai` asks for both.
172
+
173
+ | Tool | |
174
+ |---|---|
175
+ | `list_projects`, `list_pipelines` | Projects and their pipelines |
176
+ | `list_executions`, `get_execution` | Executions with their status, parameters, metrics, outputs and errors |
177
+ | `get_execution_logs` | The last lines of an execution's log, optionally only stderr |
178
+ | `list_outputs` | The files an execution produced, with `datum://` URIs to use as inputs |
179
+ | `compare_executions` | Metrics and differing parameters of several executions side by side |
180
+ | `start_execution`, `stop_execution` | Only with `--allow-writes` in the server's `args` in `mcp.json` |
181
+
182
+ Both servers are ordinary MCP servers: other MCP clients can use them too, for example
183
+ `uvx --from lcode-cli lcode-mcp-valohai` with `VALOHAI_TOKEN` set.
184
+
137
185
  ## Adding your own servers
138
186
 
139
187
  Any MCP server works. For a remote server, give its URL; for a local one, the command that starts it:
@@ -12,7 +12,7 @@ lcode
12
12
 
13
13
  ```text
14
14
  ╭──────────────────────────────────────────────╮
15
- │ lcode v0.10.0 — local coding agent │
15
+ │ lcode v0.11.0 — local coding agent │
16
16
  │ │
17
17
  │ model lcode-qwen3.6-35b │
18
18
  │ context 256K tokens │
@@ -35,6 +35,9 @@ dependencies = [
35
35
  "tomli>=2.0; python_version < '3.11'",
36
36
  ]
37
37
 
38
+ [project.optional-dependencies]
39
+ encord = ["encord>=0.1.200"] # for lcode-mcp-encord
40
+
38
41
  [project.urls]
39
42
  Homepage = "https://nasser1941.github.io/lcode/"
40
43
  Documentation = "https://nasser1941.github.io/lcode/"
@@ -44,6 +47,8 @@ Changelog = "https://github.com/nasser1941/lcode/blob/main/CHANGELOG.md"
44
47
 
45
48
  [project.scripts]
46
49
  lcode = "lcode.cli:main"
50
+ lcode-mcp-valohai = "lcode.servers.valohai:main"
51
+ lcode-mcp-encord = "lcode.servers.encord:main"
47
52
 
48
53
  [dependency-groups]
49
54
  dev = ["pytest>=8", "ruff>=0.6"]
@@ -1,3 +1,3 @@
1
1
  """lcode — a local-first terminal coding agent powered by open-weight models via Ollama."""
2
2
 
3
- __version__ = "0.10.0"
3
+ __version__ = "0.11.0"
@@ -99,6 +99,10 @@ def build(preset: Preset, answers: dict[str, str | None]) -> dict:
99
99
  server = copy.deepcopy(preset.server)
100
100
  for spec in preset.inputs:
101
101
  value = answers.get(spec.var)
102
+ if value and spec.var.endswith(("_URL", "_HOST")):
103
+ value = value.rstrip("/") # the URL may have a path appended, e.g. ${METABASE_URL}/api/metabase-mcp
104
+ if "://" not in value:
105
+ value = "https://" + value # people type metabase.example.com
102
106
  if value:
103
107
  server = _replace(server, spec.var, value)
104
108
  elif spec.optional and not os.environ.get(spec.var):
@@ -7,7 +7,7 @@
7
7
  # requires commands that must be installed.
8
8
  # login sign in through the browser (OAuth) after adding.
9
9
  #
10
- # Commands and URLs were checked against each project's documentation in September 2026.
10
+ # Commands and URLs were checked against each project's documentation in September and October 2026.
11
11
 
12
12
  [atlassian]
13
13
  name = "Atlassian"
@@ -224,3 +224,59 @@ homepage = "https://developers.notion.com/docs/mcp"
224
224
  login = true
225
225
  setup = "Sign in with your Notion account in the browser."
226
226
  server = { type = "http", url = "https://mcp.notion.com/mcp" }
227
+
228
+ [metabase]
229
+ name = "Metabase"
230
+ description = "Explore your data and ask questions through Metabase's semantic layer, with your Metabase permissions"
231
+ homepage = "https://www.metabase.com/docs/latest/ai/mcp"
232
+ login = true
233
+ setup = """Needs Metabase 60 or later with its MCP server switched on: an admin turns it on under Admin > AI > MCP.
234
+ Sign in with your Metabase account in the browser."""
235
+ server = { type = "http", url = "${METABASE_URL}/api/metabase-mcp" }
236
+
237
+ [[metabase.inputs]]
238
+ var = "METABASE_URL"
239
+ prompt = "Metabase address, e.g. https://metabase.example.com"
240
+
241
+ [encord]
242
+ name = "Encord"
243
+ description = "Projects, datasets, ontologies, labeling progress, tasks and labels (read-only)"
244
+ homepage = "https://nasser1941.github.io/lcode/mcp/#encord"
245
+ requires = ["uvx"]
246
+ setup = """Encord has no MCP server of its own, so this one comes with lcode (it uses the Encord SDK). It signs in with an SSH
247
+ key registered in Encord: Settings > Public keys. Read-only: add --allow-writes to its args in mcp.json to let the
248
+ model assign tasks and set priorities; lcode still asks before every call."""
249
+
250
+ [encord.server]
251
+ command = "uvx"
252
+ args = ["--python", "3.12", "--from", "lcode-cli[encord]", "lcode-mcp-encord"]
253
+ env = { ENCORD_SSH_KEY_FILE = "${ENCORD_SSH_KEY_FILE}" }
254
+
255
+ [[encord.inputs]]
256
+ var = "ENCORD_SSH_KEY_FILE"
257
+ prompt = "Path to the SSH private key registered in Encord"
258
+ default = "~/.ssh/id_ed25519"
259
+
260
+ [valohai]
261
+ name = "Valohai"
262
+ description = "ML executions with their logs, metrics and outputs, and pipelines (read-only)"
263
+ homepage = "https://nasser1941.github.io/lcode/mcp/#valohai"
264
+ requires = ["uvx"]
265
+ setup = """Valohai has no MCP server of its own, so this one comes with lcode. It needs an API token: My Profile >
266
+ Authentication > Manage tokens. Read-only: add --allow-writes to its args in mcp.json to let the model start and stop
267
+ executions; lcode still asks before every call."""
268
+
269
+ [valohai.server]
270
+ command = "uvx"
271
+ args = ["--python", "3.12", "--from", "lcode-cli", "lcode-mcp-valohai"]
272
+ env = { VALOHAI_TOKEN = "${VALOHAI_TOKEN}", VALOHAI_HOST = "${VALOHAI_HOST}" }
273
+
274
+ [[valohai.inputs]]
275
+ var = "VALOHAI_TOKEN"
276
+ prompt = "Valohai API token"
277
+ secret = true
278
+
279
+ [[valohai.inputs]]
280
+ var = "VALOHAI_HOST"
281
+ prompt = "Valohai address (Enter for app.valohai.com; for a self-hosted installation, its URL)"
282
+ optional = true
@@ -0,0 +1,152 @@
1
+ """A small MCP server over stdio, for the servers lcode ships itself (lcode.servers).
2
+
3
+ It speaks both eras of MCP, like lcode's client: the legacy `initialize` handshake and the modern
4
+ stateless `server/discover`. Tools are plain functions that return text; raising ToolFailure turns
5
+ into an error result the model can read. Tools that change something are only listed when the
6
+ server is started with --allow-writes.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import sys
13
+ import traceback
14
+ from collections.abc import Callable
15
+ from dataclasses import dataclass, field
16
+ from typing import IO
17
+
18
+ from lcode.mcp.protocol import LEGACY_VERSIONS, METHOD_NOT_FOUND, MODERN_VERSIONS
19
+
20
+ INVALID_PARAMS = -32602
21
+ MAX_RESULT = 60_000 # characters per tool result; long listings are cut with a note
22
+
23
+
24
+ class ToolFailure(Exception):
25
+ """A tool couldn't do its job; the message goes to the model."""
26
+
27
+
28
+ @dataclass
29
+ class Tool:
30
+ name: str
31
+ description: str
32
+ properties: dict
33
+ required: list[str]
34
+ fn: Callable[..., str]
35
+ writes: bool = False
36
+
37
+ def definition(self) -> dict:
38
+ return {
39
+ "name": self.name,
40
+ "description": self.description,
41
+ "inputSchema": {"type": "object", "properties": self.properties, "required": self.required},
42
+ "annotations": {"readOnlyHint": not self.writes, "destructiveHint": self.writes},
43
+ }
44
+
45
+
46
+ @dataclass
47
+ class Server:
48
+ name: str
49
+ version: str
50
+ instructions: str = ""
51
+ allow_writes: bool = False
52
+ tools: dict[str, Tool] = field(default_factory=dict)
53
+
54
+ def tool(self, name: str, description: str, properties: dict | None = None, required=(), writes: bool = False):
55
+ """Register a function as a tool (a decorator)."""
56
+
57
+ def register(fn: Callable[..., str]) -> Callable[..., str]:
58
+ self.tools[name] = Tool(name, description, properties or {}, list(required), fn, writes)
59
+ return fn
60
+
61
+ return register
62
+
63
+ def listed(self) -> list[Tool]:
64
+ return [t for t in self.tools.values() if self.allow_writes or not t.writes]
65
+
66
+ # -- JSON-RPC
67
+ def handle(self, message: dict) -> dict | None:
68
+ """The response to one message (None for notifications)."""
69
+ method, params, ident = message.get("method"), message.get("params") or {}, message.get("id")
70
+ if ident is None:
71
+ return None # a notification, such as notifications/initialized
72
+ try:
73
+ result = self.dispatch(str(method), params if isinstance(params, dict) else {})
74
+ except _RpcError as e:
75
+ return {"jsonrpc": "2.0", "id": ident, "error": {"code": e.code, "message": str(e)}}
76
+ return {"jsonrpc": "2.0", "id": ident, "result": result}
77
+
78
+ def dispatch(self, method: str, params: dict) -> dict:
79
+ info = {"name": self.name, "version": self.version}
80
+ if method == "initialize":
81
+ asked = str(params.get("protocolVersion") or "")
82
+ return {
83
+ "protocolVersion": asked if asked in LEGACY_VERSIONS else LEGACY_VERSIONS[0],
84
+ "capabilities": {"tools": {}},
85
+ "serverInfo": info,
86
+ "instructions": self.instructions,
87
+ }
88
+ if method == "server/discover":
89
+ return {
90
+ "supportedVersions": list(MODERN_VERSIONS),
91
+ "capabilities": {"tools": {}},
92
+ "instructions": self.instructions,
93
+ "_meta": {"io.modelcontextprotocol/serverInfo": info},
94
+ }
95
+ if method == "ping":
96
+ return {}
97
+ if method == "tools/list":
98
+ return {"tools": [t.definition() for t in self.listed()]}
99
+ if method == "tools/call":
100
+ return self.call(str(params.get("name", "")), params.get("arguments") or {})
101
+ raise _RpcError(METHOD_NOT_FOUND, f"unknown method {method}")
102
+
103
+ def call(self, name: str, arguments: dict) -> dict:
104
+ tool = self.tools.get(name)
105
+ if tool is None or tool not in self.listed():
106
+ hint = " (start the server with --allow-writes to enable it)" if tool else ""
107
+ return _text(f"Unknown tool {name}{hint}", error=True)
108
+ if not isinstance(arguments, dict):
109
+ raise _RpcError(INVALID_PARAMS, "arguments must be an object")
110
+ unknown = set(arguments) - set(tool.properties)
111
+ missing = [r for r in tool.required if arguments.get(r) in (None, "")]
112
+ if unknown or missing:
113
+ problems = [f"unknown argument(s): {', '.join(sorted(unknown))}"] if unknown else []
114
+ problems += [f"missing: {', '.join(missing)}"] if missing else []
115
+ return _text(f"Bad arguments for {name}: {'; '.join(problems)}", error=True)
116
+ try:
117
+ text = tool.fn(**arguments)
118
+ except ToolFailure as e:
119
+ return _text(str(e), error=True)
120
+ except Exception as e: # report, don't crash the server
121
+ traceback.print_exc(file=sys.stderr)
122
+ return _text(f"{type(e).__name__}: {e}", error=True)
123
+ if len(text) > MAX_RESULT:
124
+ text = text[:MAX_RESULT] + "\n… (cut off: ask for less, e.g. with a smaller limit or a filter)"
125
+ return _text(text)
126
+
127
+ def serve(self, stdin: IO[str] | None = None, stdout: IO[str] | None = None) -> None:
128
+ """Answer newline-delimited JSON-RPC on stdin/stdout until stdin closes."""
129
+ stdin, stdout = stdin or sys.stdin, stdout or sys.stdout
130
+ for line in stdin:
131
+ line = line.strip()
132
+ if not line:
133
+ continue
134
+ try:
135
+ message = json.loads(line)
136
+ except json.JSONDecodeError:
137
+ response = {"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": "parse error"}}
138
+ else:
139
+ response = self.handle(message) if isinstance(message, dict) else None
140
+ if response is not None:
141
+ stdout.write(json.dumps(response) + "\n")
142
+ stdout.flush()
143
+
144
+
145
+ class _RpcError(Exception):
146
+ def __init__(self, code: int, message: str):
147
+ super().__init__(message)
148
+ self.code = code
149
+
150
+
151
+ def _text(text: str, error: bool = False) -> dict:
152
+ return {"content": [{"type": "text", "text": text}], "isError": error}
@@ -0,0 +1 @@
1
+ """MCP servers that lcode ships for services that have none of their own."""