letscode 0.5.0__tar.gz → 0.6.1__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.
- {letscode-0.5.0 → letscode-0.6.1}/PKG-INFO +5 -4
- {letscode-0.5.0 → letscode-0.6.1}/README.md +3 -1
- letscode-0.6.1/plugins/letscode-mcp/README.md +65 -0
- letscode-0.6.1/plugins/letscode-mcp/pyproject.toml +42 -0
- letscode-0.6.1/plugins/letscode-mcp/src/letscode_mcp/__init__.py +5 -0
- letscode-0.6.1/plugins/letscode-mcp/src/letscode_mcp/plugin.py +304 -0
- letscode-0.6.1/plugins/letscode-mcp/tests/test_plugin.py +258 -0
- letscode-0.6.1/plugins/letscode-mcp/uv.lock +1036 -0
- {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-memory/uv.lock +43 -42
- {letscode-0.5.0 → letscode-0.6.1}/pyproject.toml +8 -9
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/agent/loop.py +3 -8
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/agent/tools.py +3 -40
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/cli/rpc.py +6 -4
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/plugins/registries.py +54 -83
- letscode-0.5.0/.builds/alpine.yml +0 -25
- letscode-0.5.0/.builds/ubuntu-2404.yml +0 -38
- letscode-0.5.0/.cache/.gitignore +0 -1
- letscode-0.5.0/.cache/10318616000990552681 +0 -4
- letscode-0.5.0/.cache/10493891611479424259 +0 -4
- letscode-0.5.0/.cache/10702343394967673498 +0 -4
- letscode-0.5.0/.cache/11010100411556762066 +0 -4
- letscode-0.5.0/.cache/11154141664771110464 +0 -4
- letscode-0.5.0/.cache/11168941369259313220 +0 -80
- letscode-0.5.0/.cache/11992942182152933888 +0 -144
- letscode-0.5.0/.cache/12059922036673638020 +0 -4
- letscode-0.5.0/.cache/1249039435107563575 +0 -161
- letscode-0.5.0/.cache/12576105843982642077 +0 -4
- letscode-0.5.0/.cache/14203930344178090297 +0 -4
- letscode-0.5.0/.cache/1563633362711557789 +0 -4
- letscode-0.5.0/.cache/15661711846603463071 +0 -207
- letscode-0.5.0/.cache/15908712471909210796 +0 -178
- letscode-0.5.0/.cache/16912669318574488615 +0 -80
- letscode-0.5.0/.cache/16988395898165525092 +0 -419
- letscode-0.5.0/.cache/17150484726184191142 +0 -128
- letscode-0.5.0/.cache/17419555459594506489 +0 -4
- letscode-0.5.0/.cache/2041198230313398573 +0 -4
- letscode-0.5.0/.cache/3072703261793992529 +0 -4
- letscode-0.5.0/.cache/3151054395951844290 +0 -4
- letscode-0.5.0/.cache/3226154870166803660 +0 -4
- letscode-0.5.0/.cache/3476900567878811119 +0 -4
- letscode-0.5.0/.cache/3510264687931475889 +0 -160
- letscode-0.5.0/.cache/3520474981437140632 +0 -112
- letscode-0.5.0/.cache/3541202890535126406 +0 -96
- letscode-0.5.0/.cache/3728666290973476153 +0 -80
- letscode-0.5.0/.cache/4780842327402266928 +0 -64
- letscode-0.5.0/.cache/5187878047780114009 +0 -161
- letscode-0.5.0/.cache/6069231736669146243 +0 -4
- letscode-0.5.0/.cache/6463951297718265957 +0 -31
- letscode-0.5.0/.cache/6479415784420637704 +0 -4
- letscode-0.5.0/.cache/6764327636681606151 +0 -4
- letscode-0.5.0/.cache/6789241458458765738 +0 -354
- letscode-0.5.0/.cache/8444450551902475297 +0 -113
- letscode-0.5.0/.cache/8863633364771264685 +0 -112
- letscode-0.5.0/.cache/9378231046984033364 +0 -177
- letscode-0.5.0/.cache/9810942886417102030 +0 -4
- letscode-0.5.0/.github/workflows/ci.yml +0 -36
- letscode-0.5.0/.github/workflows/docs.yml +0 -29
- letscode-0.5.0/.pre-commit-config.yaml +0 -33
- letscode-0.5.0/CHANGES.md +0 -123
- letscode-0.5.0/CLAUDE.md +0 -85
- letscode-0.5.0/Makefile +0 -50
- letscode-0.5.0/docs/about/changelog.md +0 -39
- letscode-0.5.0/docs/about/index.md +0 -54
- letscode-0.5.0/docs/about/lessons-learned.md +0 -56
- letscode-0.5.0/docs/about/related-projects.md +0 -69
- letscode-0.5.0/docs/about/roadmap.md +0 -98
- letscode-0.5.0/docs/architecture/agent-loop.md +0 -97
- letscode-0.5.0/docs/architecture/extension-model.md +0 -194
- letscode-0.5.0/docs/architecture/index.md +0 -69
- letscode-0.5.0/docs/getting-started.md +0 -136
- letscode-0.5.0/docs/index.md +0 -113
- letscode-0.5.0/docs/plugins/authoring.md +0 -542
- letscode-0.5.0/docs/plugins/index.md +0 -85
- letscode-0.5.0/docs/plugins/memory.md +0 -79
- letscode-0.5.0/docs/user-guide/configuration.md +0 -108
- letscode-0.5.0/docs/user-guide/index.md +0 -36
- letscode-0.5.0/docs/user-guide/sessions.md +0 -55
- letscode-0.5.0/docs/user-guide/skills.md +0 -130
- letscode-0.5.0/docs/user-guide/slash-commands.md +0 -87
- letscode-0.5.0/local-notes/playbooks/litestar-dishka/CHECKLISTS.md +0 -131
- letscode-0.5.0/local-notes/playbooks/litestar-dishka/COMMON-GOTCHAS.md +0 -296
- letscode-0.5.0/local-notes/playbooks/litestar-dishka/INDEX.md +0 -90
- letscode-0.5.0/local-notes/playbooks/litestar-dishka/advanced-alchemy.md +0 -1558
- letscode-0.5.0/local-notes/playbooks/litestar-dishka/configuration.md +0 -858
- letscode-0.5.0/local-notes/playbooks/litestar-dishka/dishka.md +0 -244
- letscode-0.5.0/local-notes/playbooks/litestar-dishka/integration.md +0 -135
- letscode-0.5.0/local-notes/playbooks/litestar-dishka/litestar-views.md +0 -322
- letscode-0.5.0/local-notes/playbooks/litestar-dishka/litestar.md +0 -408
- letscode-0.5.0/local-notes/playbooks/litestar-dishka/stack-guide.md +0 -1060
- letscode-0.5.0/local-notes/playbooks/litestar-dishka/testing.md +0 -1308
- letscode-0.5.0/local-notes/playbooks/litestar-dishka/workflows.md +0 -707
- letscode-0.5.0/local-notes/playbooks/python/4-rules-design.md +0 -159
- letscode-0.5.0/local-notes/playbooks/python/CHECKLISTS.md +0 -171
- letscode-0.5.0/local-notes/playbooks/python/INDEX.md +0 -72
- letscode-0.5.0/local-notes/playbooks/python/coding-guidelines.md +0 -301
- letscode-0.5.0/local-notes/playbooks/python/design-patterns.md +0 -101
- letscode-0.5.0/local-notes/playbooks/python/error-messages.md +0 -197
- letscode-0.5.0/local-notes/playbooks/python/monorepo.md +0 -513
- letscode-0.5.0/local-notes/playbooks/python/nouns-and-verbs.md +0 -125
- letscode-0.5.0/local-notes/playbooks/python/testing.md +0 -434
- letscode-0.5.0/local-notes/playbooks/webdev/CHECKLISTS.md +0 -175
- letscode-0.5.0/local-notes/playbooks/webdev/INDEX.md +0 -74
- letscode-0.5.0/local-notes/playbooks/webdev/api-design.md +0 -405
- letscode-0.5.0/local-notes/playbooks/webdev/deployment.md +0 -499
- letscode-0.5.0/local-notes/playbooks/webdev/hybrid-ui.md +0 -194
- letscode-0.5.0/local-notes/playbooks/webdev/production-patterns.md +0 -577
- letscode-0.5.0/local-notes/playbooks/webdev/security.md +0 -480
- letscode-0.5.0/notes/01-vision.md +0 -323
- letscode-0.5.0/notes/02-design.md +0 -1143
- letscode-0.5.0/notes/03-plan.md +0 -383
- letscode-0.5.0/notes/04-lessons-learned.md +0 -274
- letscode-0.5.0/notes/05-gap-vs-pi.md +0 -284
- letscode-0.5.0/notes/06-v0.2-plan.md +0 -183
- letscode-0.5.0/notes/07-v0.3-plan.md +0 -204
- letscode-0.5.0/notes/08-extension-model.md +0 -228
- letscode-0.5.0/notes/09-ergonomics-punchlist.md +0 -96
- letscode-0.5.0/notes/10-v0.4-plan.md +0 -250
- letscode-0.5.0/notes/11-plugin-contract-v0.4-findings.md +0 -110
- letscode-0.5.0/notes/12-textual-frontend.md +0 -339
- letscode-0.5.0/notes/13-textual-test-drive.md +0 -105
- letscode-0.5.0/notes/14-vs-anthropic-playbook.md +0 -65
- letscode-0.5.0/notes/15-prompt-caching.md +0 -57
- letscode-0.5.0/notes/16-config-schema.md +0 -96
- letscode-0.5.0/notes/17-rpc-protocol.md +0 -135
- letscode-0.5.0/notes/18-v0.5-plan.md +0 -147
- letscode-0.5.0/notes/19-release-runbook.md +0 -23
- letscode-0.5.0/notes/plans/2026-W21.md +0 -46
- letscode-0.5.0/notes/plans/textual-tui.md +0 -69
- letscode-0.5.0/notes/playbooks/INDEX.md +0 -129
- letscode-0.5.0/notes/playbooks/generic-python/4-rules-design.md +0 -159
- letscode-0.5.0/notes/playbooks/generic-python/CHECKLISTS.md +0 -173
- letscode-0.5.0/notes/playbooks/generic-python/INDEX.md +0 -72
- letscode-0.5.0/notes/playbooks/generic-python/coding-guidelines.md +0 -301
- letscode-0.5.0/notes/playbooks/generic-python/design-patterns.md +0 -101
- letscode-0.5.0/notes/playbooks/generic-python/error-messages.md +0 -197
- letscode-0.5.0/notes/playbooks/generic-python/monorepo.md +0 -513
- letscode-0.5.0/notes/playbooks/generic-python/nouns-and-verbs.md +0 -125
- letscode-0.5.0/notes/playbooks/generic-python/testing.md +0 -434
- letscode-0.5.0/notes/playbooks/pluggy/CHECKLISTS.md +0 -126
- letscode-0.5.0/notes/playbooks/pluggy/COMMON-GOTCHAS.md +0 -155
- letscode-0.5.0/notes/playbooks/pluggy/INDEX.md +0 -88
- letscode-0.5.0/notes/playbooks/pluggy/plugin-system.md +0 -870
- letscode-0.5.0/notes/plugins-guide.md +0 -554
- letscode-0.5.0/notes/test-drive.md +0 -116
- letscode-0.5.0/notes/weekly/00readme.md +0 -104
- letscode-0.5.0/notes/weekly/2026-W20.md +0 -96
- letscode-0.5.0/noxfile.py +0 -27
- letscode-0.5.0/ruff.toml +0 -53
- letscode-0.5.0/scripts/verify_plugin.py +0 -93
- letscode-0.5.0/scripts/verify_wheel.py +0 -143
- letscode-0.5.0/tests/__init__.py +0 -0
- letscode-0.5.0/tests/_helpers.py +0 -170
- letscode-0.5.0/tests/a_unit/__init__.py +0 -0
- letscode-0.5.0/tests/a_unit/test_builtin_commands.py +0 -602
- letscode-0.5.0/tests/a_unit/test_builtin_plugin.py +0 -164
- letscode-0.5.0/tests/a_unit/test_cli.py +0 -845
- letscode-0.5.0/tests/a_unit/test_command_dispatch.py +0 -274
- letscode-0.5.0/tests/a_unit/test_compaction.py +0 -545
- letscode-0.5.0/tests/a_unit/test_config.py +0 -285
- letscode-0.5.0/tests/a_unit/test_context_files.py +0 -172
- letscode-0.5.0/tests/a_unit/test_execution_env.py +0 -199
- letscode-0.5.0/tests/a_unit/test_fake_llm.py +0 -84
- letscode-0.5.0/tests/a_unit/test_footer.py +0 -139
- letscode-0.5.0/tests/a_unit/test_hooks.py +0 -211
- letscode-0.5.0/tests/a_unit/test_hookspecs.py +0 -125
- letscode-0.5.0/tests/a_unit/test_llm_client.py +0 -574
- letscode-0.5.0/tests/a_unit/test_plugin_manager.py +0 -464
- letscode-0.5.0/tests/a_unit/test_session.py +0 -413
- letscode-0.5.0/tests/a_unit/test_skill_discovery.py +0 -154
- letscode-0.5.0/tests/a_unit/test_skill_loader.py +0 -192
- letscode-0.5.0/tests/a_unit/test_skill_synth.py +0 -246
- letscode-0.5.0/tests/a_unit/test_smoke.py +0 -30
- letscode-0.5.0/tests/a_unit/test_stream.py +0 -138
- letscode-0.5.0/tests/a_unit/test_tool_bash.py +0 -183
- letscode-0.5.0/tests/a_unit/test_tool_edit.py +0 -231
- letscode-0.5.0/tests/a_unit/test_tool_read.py +0 -164
- letscode-0.5.0/tests/a_unit/test_tool_write.py +0 -164
- letscode-0.5.0/tests/a_unit/test_tools.py +0 -287
- letscode-0.5.0/tests/a_unit/test_types.py +0 -235
- letscode-0.5.0/tests/b_integration/__init__.py +0 -0
- letscode-0.5.0/tests/b_integration/test_agent.py +0 -787
- letscode-0.5.0/tests/b_integration/test_agent_loop.py +0 -440
- letscode-0.5.0/tests/b_integration/test_agent_loop_tools.py +0 -473
- letscode-0.5.0/tests/b_integration/test_basic_frontend.py +0 -2361
- letscode-0.5.0/tests/b_integration/test_lifecycle_hooks.py +0 -857
- letscode-0.5.0/tests/b_integration/test_rpc_mode.py +0 -351
- letscode-0.5.0/tests/b_integration/test_rpc_subprocess.py +0 -219
- letscode-0.5.0/tests/c_e2e/__init__.py +0 -0
- letscode-0.5.0/tests/c_e2e/test_smoke.py +0 -246
- letscode-0.5.0/tests/conftest.py +0 -79
- letscode-0.5.0/tests/fixtures/config/sample.toml +0 -11
- letscode-0.5.0/tests/fixtures/llm/README.md +0 -47
- letscode-0.5.0/tests/fixtures/llm/hello-tool.jsonl +0 -5
- letscode-0.5.0/tests/fixtures/llm/hello.jsonl +0 -5
- letscode-0.5.0/uv.lock +0 -1326
- letscode-0.5.0/zensical.toml +0 -161
- {letscode-0.5.0 → letscode-0.6.1}/.gitignore +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-goal/README.md +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-goal/pyproject.toml +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-goal/src/letscode_goal/__init__.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-goal/src/letscode_goal/plugin.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-goal/src/letscode_goal/state.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-goal/tests/test_plugin.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-goal/uv.lock +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-memory/README.md +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-memory/pyproject.toml +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-memory/src/letscode_memory/__init__.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-memory/src/letscode_memory/plugin.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-memory/src/letscode_memory/store.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-memory/tests/test_plugin.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-textual/README.md +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-textual/pyproject.toml +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-textual/src/letscode_textual/__init__.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-textual/src/letscode_textual/app.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-textual/src/letscode_textual/frontend.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-textual/src/letscode_textual/plugin.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-textual/tests/test_plugin.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/plugins/letscode-textual/uv.lock +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/__main__.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/agent/__init__.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/agent/agent.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/agent/compaction.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/agent/events.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/agent/execution_env.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/agent/hooks.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/agent/hookspecs.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/agent/messages.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/agent/state.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/cli/__init__.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/cli/app.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/commands/__init__.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/commands/builtin.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/commands/dispatch.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/config/__init__.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/config/loader.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/context/__init__.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/context/files.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/frontends/__init__.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/frontends/basic/__init__.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/frontends/basic/footer.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/frontends/basic/tui.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/frontends/protocol.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/llm/__init__.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/llm/client.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/llm/errors.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/llm/models.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/llm/stream.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/llm/types.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/plugins/__init__.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/plugins/builtin.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/plugins/manager.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/session/__init__.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/session/format.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/session/store.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/skills/__init__.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/skills/discovery.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/skills/loader.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/skills/synth.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/tools/__init__.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/tools/bash.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/tools/builtin.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/tools/edit.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/tools/read.py +0 -0
- {letscode-0.5.0 → letscode-0.6.1}/src/letscode/tools/write.py +0 -0
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: letscode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: A minimal coding agent for the terminal.
|
|
5
|
-
Project-URL: Homepage, https://git.sr.ht/~sfermigier/letscode
|
|
6
|
-
Project-URL: Source Code, https://git.sr.ht/~sfermigier/letscode
|
|
7
5
|
Author-email: Stefane Fermigier <sf@abilian.com>
|
|
8
6
|
Requires-Python: >=3.12
|
|
9
7
|
Requires-Dist: aiofiles>=24.0
|
|
@@ -13,6 +11,7 @@ Requires-Dist: openai>=1.0
|
|
|
13
11
|
Requires-Dist: pluggy>=1.5
|
|
14
12
|
Requires-Dist: prompt-toolkit>=3.0
|
|
15
13
|
Requires-Dist: pydantic>=2.0
|
|
14
|
+
Requires-Dist: pyyaml>=6.0
|
|
16
15
|
Requires-Dist: rich>=13.0
|
|
17
16
|
Description-Content-Type: text/markdown
|
|
18
17
|
|
|
@@ -20,10 +19,12 @@ Description-Content-Type: text/markdown
|
|
|
20
19
|
|
|
21
20
|
A minimal, OpenAI-compatible coding agent for the terminal — written in Python. Point it at any OpenAI-API-compatible endpoint (Ollama, Fireworks, OpenRouter, vLLM, llama.cpp's `llama-server`, …) and get a streaming agent loop with the four tools that cover 95% of coding sessions — `read`, `write`, `edit`, `bash` — plus skills, slash commands, and a plugin system you can extend.
|
|
22
21
|
|
|
23
|
-
**Status:** v0.
|
|
22
|
+
**Status:** v0.6, alpha.
|
|
24
23
|
|
|
25
24
|
The full docs live in [`docs/`](docs/) and build via [Zensical](https://zensical.org) — run `make docs-serve` for a live preview, or `make docs` for a static build.
|
|
26
25
|
|
|
26
|
+
Or go to <https://letscode.hop3.abilian.com> for the online doc.
|
|
27
|
+
|
|
27
28
|
## What it does
|
|
28
29
|
|
|
29
30
|
- **Streaming agent loop** with parallel tool execution.
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
A minimal, OpenAI-compatible coding agent for the terminal — written in Python. Point it at any OpenAI-API-compatible endpoint (Ollama, Fireworks, OpenRouter, vLLM, llama.cpp's `llama-server`, …) and get a streaming agent loop with the four tools that cover 95% of coding sessions — `read`, `write`, `edit`, `bash` — plus skills, slash commands, and a plugin system you can extend.
|
|
4
4
|
|
|
5
|
-
**Status:** v0.
|
|
5
|
+
**Status:** v0.6, alpha.
|
|
6
6
|
|
|
7
7
|
The full docs live in [`docs/`](docs/) and build via [Zensical](https://zensical.org) — run `make docs-serve` for a live preview, or `make docs` for a static build.
|
|
8
8
|
|
|
9
|
+
Or go to <https://letscode.hop3.abilian.com> for the online doc.
|
|
10
|
+
|
|
9
11
|
## What it does
|
|
10
12
|
|
|
11
13
|
- **Streaming agent loop** with parallel tool execution.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# letscode-mcp
|
|
2
|
+
|
|
3
|
+
A [Model Context Protocol](https://modelcontextprotocol.io/) client plugin for [letscode](https://github.com/abilian/letscode). Configure an MCP server in TOML; restart `letscode`; the server's tools show up in `/tools` and the model can call them — same shape as any other letscode tool.
|
|
4
|
+
|
|
5
|
+
**Scope (v0.6):** stdio transport, tools only (resources and prompts deferred). Design: `notes/21-mcp-integration.md` in the letscode repo.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
uv tool install letscode-mcp # or: pipx install letscode-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Once installed alongside `letscode`, the plugin is auto-discovered via the `letscode` entry-point group — no extra wiring.
|
|
14
|
+
|
|
15
|
+
## Configure
|
|
16
|
+
|
|
17
|
+
Edit `~/.letscode/config.toml` (or `.letscode/config.toml` in a project — project overrides user per key):
|
|
18
|
+
|
|
19
|
+
```toml
|
|
20
|
+
[mcp.servers.filesystem]
|
|
21
|
+
command = "uvx"
|
|
22
|
+
args = ["mcp-server-filesystem", "/path/to/project"]
|
|
23
|
+
|
|
24
|
+
[mcp.servers.github]
|
|
25
|
+
command = "npx"
|
|
26
|
+
args = ["-y", "@modelcontextprotocol/server-github"]
|
|
27
|
+
env = { GITHUB_TOKEN = "$GITHUB_TOKEN" } # $VAR / ${VAR} expand from your env
|
|
28
|
+
prefix = "gh_" # optional; default = "mcp_<server>_"
|
|
29
|
+
timeout = 10 # seconds for initialize + list_tools
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Per-server keys:
|
|
33
|
+
|
|
34
|
+
| key | required | meaning |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `command` | yes | executable to spawn (resolved on `PATH`) |
|
|
37
|
+
| `args` | no | command-line args (default `[]`) |
|
|
38
|
+
| `env` | no | extra env vars; values support `$VAR` / `${VAR}` expansion; unset → empty string |
|
|
39
|
+
| `prefix` | no | tool-name prefix override; default `mcp_<server>_` |
|
|
40
|
+
| `timeout` | no | seconds for the startup handshake (default `10`) |
|
|
41
|
+
|
|
42
|
+
## Tool naming
|
|
43
|
+
|
|
44
|
+
Discovered tools are namespaced to avoid colliding with letscode built-ins (`read`, `write`, `edit`, `bash`) and with each other. A server keyed `filesystem` exposing `read_file` registers as `mcp_filesystem_read_file`. Use `prefix = "fs_"` to shorten if the token cost matters more than the safety margin.
|
|
45
|
+
|
|
46
|
+
## Failure model
|
|
47
|
+
|
|
48
|
+
Every failure surfaces as a one-line stderr warning at registration or a `ToolResult(is_error=True)` at call time. Never a traceback into the agent loop.
|
|
49
|
+
|
|
50
|
+
| When | Failure | You see |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| register | `command` not on `PATH` | warning, server skipped, other servers continue |
|
|
53
|
+
| register | startup exceeds `timeout` | warning, server skipped |
|
|
54
|
+
| call | server crashed | `is_error=True` carrying the error message |
|
|
55
|
+
| call | server returns `isError=True` | `is_error=True` carrying the server's content |
|
|
56
|
+
|
|
57
|
+
No auto-restart in v0.6 — restart `letscode` to recover.
|
|
58
|
+
|
|
59
|
+
## Trust model
|
|
60
|
+
|
|
61
|
+
A configured MCP server runs with your full shell power, same model as letscode's built-in `bash` tool. Only configure servers you trust to read your code, write your files, and hit network endpoints on your behalf.
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "letscode-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "MCP (Model Context Protocol) client plugin for letscode. Surface tools from any MCP server as native letscode tools."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "letscode contributors" },
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"letscode>=0.6",
|
|
12
|
+
"pluggy>=1.5",
|
|
13
|
+
"pydantic>=2.0",
|
|
14
|
+
"mcp>=1.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.entry-points.letscode]
|
|
18
|
+
mcp = "letscode_mcp.plugin"
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["hatchling"]
|
|
22
|
+
build-backend = "hatchling.build"
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.wheel]
|
|
25
|
+
packages = ["src/letscode_mcp"]
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel.sources]
|
|
28
|
+
"src" = ""
|
|
29
|
+
|
|
30
|
+
# Dev only: resolve letscode to the local checkout when this plugin's own
|
|
31
|
+
# venv is built. uv ignores [tool.uv.sources] in published wheels.
|
|
32
|
+
[tool.uv.sources]
|
|
33
|
+
letscode = { path = "../..", editable = true }
|
|
34
|
+
|
|
35
|
+
[dependency-groups]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest>=9.0.3",
|
|
38
|
+
"pytest-asyncio>=0.24",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""letscode-mcp: discover MCP servers from TOML config, expose their tools.
|
|
2
|
+
|
|
3
|
+
Design: ``notes/21-mcp-integration.md``. One file does it all — config
|
|
4
|
+
read, background asyncio loop, MCP discovery, tool wrapping. The plugin
|
|
5
|
+
contract (``letscode_register_tools``) is sync, the MCP SDK is async; a
|
|
6
|
+
single daemon thread owning a dedicated event loop bridges the two
|
|
7
|
+
(``asyncio.run_coroutine_threadsafe``).
|
|
8
|
+
|
|
9
|
+
Failure model is uniform: bad config, missing binary, hung server,
|
|
10
|
+
malformed schema, mid-session crash — every one becomes a one-line
|
|
11
|
+
stderr warning at registration *or* a ``ToolResult(is_error=True)`` at
|
|
12
|
+
call time. Never a traceback into the agent loop.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import atexit
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import sys
|
|
23
|
+
import threading
|
|
24
|
+
from typing import TYPE_CHECKING, Any
|
|
25
|
+
|
|
26
|
+
import pluggy
|
|
27
|
+
from pydantic import BaseModel, ConfigDict
|
|
28
|
+
|
|
29
|
+
from letscode.agent.tools import ToolResult
|
|
30
|
+
from letscode.config import load_config
|
|
31
|
+
from letscode.llm.types import TextPart
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from collections.abc import Coroutine
|
|
35
|
+
from concurrent.futures import Future as _CFuture
|
|
36
|
+
|
|
37
|
+
from letscode.agent.tools import ToolContext, ToolRegistry
|
|
38
|
+
|
|
39
|
+
_logger = logging.getLogger(__name__)
|
|
40
|
+
hookimpl = pluggy.HookimplMarker("letscode")
|
|
41
|
+
|
|
42
|
+
_DEFAULT_TIMEOUT = 10.0
|
|
43
|
+
# $VAR or ${VAR}. Empty string when unset — keeps configs portable
|
|
44
|
+
# instead of erroring on a host that doesn't have an optional secret.
|
|
45
|
+
_ENV_RE = re.compile(r"\$(?:\{([^}]+)\}|([A-Za-z_][A-Za-z0-9_]*))")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Background asyncio loop (one daemon thread for all MCP sessions)
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
_bg_loop: asyncio.AbstractEventLoop | None = None
|
|
53
|
+
_bg_thread: threading.Thread | None = None
|
|
54
|
+
_bg_lock = threading.Lock()
|
|
55
|
+
# Stop-events for every live session task. atexit flips them to drain
|
|
56
|
+
# the bg loop cleanly on process exit.
|
|
57
|
+
_stop_events: list[asyncio.Event] = []
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _ensure_bg_loop() -> asyncio.AbstractEventLoop:
|
|
61
|
+
"""Start the daemon thread + event loop on first use; idempotent."""
|
|
62
|
+
global _bg_loop, _bg_thread # noqa: PLW0603 — process-wide singleton, locked init
|
|
63
|
+
with _bg_lock:
|
|
64
|
+
if _bg_loop is not None:
|
|
65
|
+
return _bg_loop
|
|
66
|
+
loop = asyncio.new_event_loop()
|
|
67
|
+
thread = threading.Thread(
|
|
68
|
+
target=loop.run_forever, name="letscode-mcp", daemon=True
|
|
69
|
+
)
|
|
70
|
+
thread.start()
|
|
71
|
+
_bg_loop = loop
|
|
72
|
+
_bg_thread = thread
|
|
73
|
+
atexit.register(_shutdown)
|
|
74
|
+
return loop
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _run_in_bg(coro: Coroutine[Any, Any, Any], *, timeout: float | None = None) -> Any: # noqa: ANN401
|
|
78
|
+
"""Run a coroutine on the bg loop from a sync caller; block for result."""
|
|
79
|
+
loop = _ensure_bg_loop()
|
|
80
|
+
fut: _CFuture[Any] = asyncio.run_coroutine_threadsafe(coro, loop)
|
|
81
|
+
return fut.result(timeout=timeout)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _shutdown() -> None:
|
|
85
|
+
"""Best-effort cleanup. Subprocesses die with the parent regardless."""
|
|
86
|
+
loop = _bg_loop
|
|
87
|
+
if loop is None:
|
|
88
|
+
return
|
|
89
|
+
for stop in _stop_events:
|
|
90
|
+
loop.call_soon_threadsafe(stop.set)
|
|
91
|
+
# ponytail: no join — daemon thread, OS reaps the subprocess children
|
|
92
|
+
loop.call_soon_threadsafe(loop.stop)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
# Config helpers
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _expand_env(env_in: dict[str, str]) -> dict[str, str]:
|
|
101
|
+
"""Expand ``$VAR`` / ``${VAR}`` in env values against ``os.environ``."""
|
|
102
|
+
return {
|
|
103
|
+
k: _ENV_RE.sub(lambda m: os.environ.get(m.group(1) or m.group(2), ""), v)
|
|
104
|
+
for k, v in env_in.items()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
# Pydantic params model synthesised from an MCP tool's inputSchema
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _params_model(class_name: str, schema: dict[str, Any]) -> type[BaseModel]:
|
|
114
|
+
"""A permissive ``BaseModel`` whose ``model_json_schema`` is the MCP schema.
|
|
115
|
+
|
|
116
|
+
The LLM sees the real schema (so it knows required fields and types);
|
|
117
|
+
incoming arg dicts are accepted verbatim via ``extra="allow"``. The
|
|
118
|
+
MCP server itself validates on receive — re-validating here would
|
|
119
|
+
just be a second source of truth that could disagree.
|
|
120
|
+
"""
|
|
121
|
+
fixed = schema or {"type": "object", "properties": {}}
|
|
122
|
+
|
|
123
|
+
class _Params(BaseModel):
|
|
124
|
+
model_config = ConfigDict(extra="allow")
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def model_json_schema(cls, **_kw: Any) -> dict[str, Any]: # type: ignore[override] # noqa: ANN401
|
|
128
|
+
return fixed
|
|
129
|
+
|
|
130
|
+
_Params.__name__ = class_name
|
|
131
|
+
return _Params
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
# letscode Tool wrapping one discovered MCP tool
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class _McpTool:
|
|
140
|
+
"""A letscode Tool whose ``execute`` marshals a call to the bg loop."""
|
|
141
|
+
|
|
142
|
+
execution_mode = "parallel"
|
|
143
|
+
|
|
144
|
+
def __init__( # noqa: PLR0913 — six small kwargs read better than a config object
|
|
145
|
+
self,
|
|
146
|
+
*,
|
|
147
|
+
name: str,
|
|
148
|
+
description: str,
|
|
149
|
+
parameters: type[BaseModel],
|
|
150
|
+
session: Any, # noqa: ANN401 — opaque mcp ClientSession
|
|
151
|
+
remote_name: str,
|
|
152
|
+
timeout: float,
|
|
153
|
+
) -> None:
|
|
154
|
+
self.name = name
|
|
155
|
+
self.description = description
|
|
156
|
+
self.parameters = parameters
|
|
157
|
+
self._session = session
|
|
158
|
+
self._remote_name = remote_name
|
|
159
|
+
self._timeout = timeout
|
|
160
|
+
|
|
161
|
+
async def execute(self, params: BaseModel, ctx: ToolContext) -> ToolResult:
|
|
162
|
+
del ctx
|
|
163
|
+
args = params.model_dump()
|
|
164
|
+
try:
|
|
165
|
+
cfut = asyncio.run_coroutine_threadsafe(self._call(args), _ensure_bg_loop())
|
|
166
|
+
return await asyncio.wrap_future(cfut)
|
|
167
|
+
except Exception as exc: # noqa: BLE001 — deliberate fail-soft; design §5
|
|
168
|
+
return ToolResult(
|
|
169
|
+
content=[TextPart(text=f"MCP call failed: {exc}")],
|
|
170
|
+
is_error=True,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
async def _call(self, args: dict[str, Any]) -> ToolResult:
|
|
174
|
+
result = await asyncio.wait_for(
|
|
175
|
+
self._session.call_tool(self._remote_name, args), timeout=self._timeout
|
|
176
|
+
)
|
|
177
|
+
parts = [TextPart(text=_part_text(p)) for p in (result.content or [])]
|
|
178
|
+
if not parts:
|
|
179
|
+
parts = [TextPart(text="")]
|
|
180
|
+
return ToolResult(
|
|
181
|
+
content=parts, is_error=bool(getattr(result, "isError", False))
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _part_text(part: Any) -> str: # noqa: ANN401 — opaque mcp content union
|
|
186
|
+
"""MCP TextContent has ``.text``; image/embedded-resource falls back to repr."""
|
|
187
|
+
return getattr(part, "text", None) or repr(part)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
# Server bring-up (runs on the bg loop)
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
async def _spawn_server(
|
|
196
|
+
server_name: str,
|
|
197
|
+
command: str,
|
|
198
|
+
args: list[str],
|
|
199
|
+
env: dict[str, str],
|
|
200
|
+
timeout: float,
|
|
201
|
+
) -> tuple[Any, list[Any], asyncio.Event]:
|
|
202
|
+
"""Open one MCP server in a long-running task.
|
|
203
|
+
|
|
204
|
+
Returns ``(session, tools, stop_event)``. The task holds the
|
|
205
|
+
``stdio_client`` + ``ClientSession`` context managers open until
|
|
206
|
+
``stop_event`` is set — the right pattern for anyio task scopes
|
|
207
|
+
(enter and exit on the same task).
|
|
208
|
+
"""
|
|
209
|
+
# ponytail: imports inside the function so the plugin module loads
|
|
210
|
+
# even when the mcp SDK is absent (rare; matters during plugin discovery).
|
|
211
|
+
from mcp import ClientSession, StdioServerParameters # noqa: PLC0415
|
|
212
|
+
from mcp.client.stdio import stdio_client # noqa: PLC0415
|
|
213
|
+
|
|
214
|
+
params = StdioServerParameters(command=command, args=args, env=env or None)
|
|
215
|
+
loop = asyncio.get_running_loop()
|
|
216
|
+
ready: asyncio.Future[tuple[Any, list[Any]]] = loop.create_future()
|
|
217
|
+
stop = asyncio.Event()
|
|
218
|
+
|
|
219
|
+
async def _serve() -> None:
|
|
220
|
+
try: # noqa: PLW0717 — one long-lived async-with is the whole body
|
|
221
|
+
async with (
|
|
222
|
+
stdio_client(params) as (read, write),
|
|
223
|
+
ClientSession(read, write) as session,
|
|
224
|
+
):
|
|
225
|
+
await asyncio.wait_for(session.initialize(), timeout=timeout)
|
|
226
|
+
listed = await asyncio.wait_for(session.list_tools(), timeout=timeout)
|
|
227
|
+
if not ready.done():
|
|
228
|
+
ready.set_result((session, list(listed.tools)))
|
|
229
|
+
await stop.wait()
|
|
230
|
+
except Exception as exc: # noqa: BLE001 — surfaces to caller via ready.set_exception
|
|
231
|
+
if not ready.done():
|
|
232
|
+
ready.set_exception(exc)
|
|
233
|
+
|
|
234
|
+
# The bg loop holds the running task; the linter's "store the handle"
|
|
235
|
+
# warning doesn't apply — the task lives until `stop` is set.
|
|
236
|
+
asyncio.create_task(_serve(), name=f"mcp-{server_name}") # noqa: RUF006
|
|
237
|
+
session, tools = await ready
|
|
238
|
+
return session, tools, stop
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
# The hook
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@hookimpl
|
|
247
|
+
def letscode_register_tools(registry: ToolRegistry) -> None:
|
|
248
|
+
"""Discover every configured MCP server, register its tools."""
|
|
249
|
+
try:
|
|
250
|
+
cfg = load_config()
|
|
251
|
+
servers = cfg.sections.get("mcp", {}).get("servers", {})
|
|
252
|
+
except Exception:
|
|
253
|
+
_logger.warning("mcp: could not read config; skipping", exc_info=True)
|
|
254
|
+
return
|
|
255
|
+
if not servers:
|
|
256
|
+
return
|
|
257
|
+
for server_name, server_cfg in servers.items():
|
|
258
|
+
_register_one_server(registry, server_name, server_cfg or {})
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _register_one_server(
|
|
262
|
+
registry: ToolRegistry, server_name: str, server_cfg: dict[str, Any]
|
|
263
|
+
) -> None:
|
|
264
|
+
command = server_cfg.get("command")
|
|
265
|
+
if not command:
|
|
266
|
+
_warn(f"server {server_name!r} missing 'command'; skipped")
|
|
267
|
+
return
|
|
268
|
+
args = list(server_cfg.get("args", []))
|
|
269
|
+
env = _expand_env(dict(server_cfg.get("env", {})))
|
|
270
|
+
prefix = server_cfg.get("prefix") or f"mcp_{server_name}_"
|
|
271
|
+
timeout = float(server_cfg.get("timeout", _DEFAULT_TIMEOUT))
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
session, tools, stop = _run_in_bg(
|
|
275
|
+
_spawn_server(server_name, command, args, env, timeout),
|
|
276
|
+
timeout=timeout + 5,
|
|
277
|
+
)
|
|
278
|
+
except Exception as exc: # noqa: BLE001 — fail-soft; design §5
|
|
279
|
+
_warn(f"server {server_name!r} ({command}) failed: {exc}")
|
|
280
|
+
return
|
|
281
|
+
_stop_events.append(stop)
|
|
282
|
+
|
|
283
|
+
for t in tools:
|
|
284
|
+
try:
|
|
285
|
+
tool_name = f"{prefix}{t.name}"
|
|
286
|
+
registry.add(
|
|
287
|
+
_McpTool(
|
|
288
|
+
name=tool_name,
|
|
289
|
+
description=t.description or "",
|
|
290
|
+
parameters=_params_model(
|
|
291
|
+
f"_McpParams_{server_name}_{t.name}",
|
|
292
|
+
dict(t.inputSchema or {}),
|
|
293
|
+
),
|
|
294
|
+
session=session,
|
|
295
|
+
remote_name=t.name,
|
|
296
|
+
timeout=timeout,
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
except Exception as exc: # noqa: BLE001 — fail-soft per tool
|
|
300
|
+
_warn(f"server {server_name!r} tool {t.name!r} skipped: {exc}")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _warn(msg: str) -> None:
|
|
304
|
+
print(f"letscode-mcp: {msg}", file=sys.stderr)
|