Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions .cursor/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,7 @@
"mcpServers": {
"local-coding-slm": {
"type": "stdio",
"command": "${workspaceFolder}/.venv/bin/python",
"args": ["${workspaceFolder}/src/local_coding_slm/server.py"],
"env": {
"OLLAMA_BASE_URL": "${env:OLLAMA_BASE_URL}",
"OLLAMA_FAST_MODEL": "${env:OLLAMA_FAST_MODEL}",
"OLLAMA_STRONG_MODEL": "${env:OLLAMA_STRONG_MODEL}",
"OLLAMA_NUM_CTX": "${env:OLLAMA_NUM_CTX}"
},
"envFile": "${workspaceFolder}/.env"
"command": "${workspaceFolder}/scripts/run_mcp.sh"
}
}
}
32 changes: 27 additions & 5 deletions .cursor/rules/local-coding-slm.mdc
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
---
description: Delegate mechanical coding work to the local-coding-slm MCP tools
description: Delegate mechanical coding work to local-coding-slm; premium reviews before apply
alwaysApply: true
---

When a coding task is mechanical (tests, boilerplate, local rename, summary),
call the local-coding-slm MCP tools instead of generating the full artifact
yourself. Prefer local_generate_tests, local_code, local_refactor,
local_explain, or local_review. Use model=fast first. Escalate to model=strong
only if the fast result is too weak. Review the tool output before applying it.
Do not send secrets, .env files, or credentials to those tools.
yourself.

If those tools are not in your tool list (Cursor Cloud Agent, hosted runner),
do the work yourself. Do not invent a local_* result.

Keep incident, architecture, live-tool, and vague work on the premium model.
Do not send secrets, .env files, or credentials.

When you call local_*:
- Attach files as `{path, content}` snippets the SLM needs. Not a repo dump.
- Use model=fast first. If the reply is unfenced or structurally wrong, retry
once on fast with a fence/structure reminder. If behavior is still wrong,
escalate to model=strong. Then stop calling local_*.
- Expect markdown fenced files with path comments. Do not apply a unified
diff from this server.
- `local_review` is notes only. It cannot approve a patch.
- If the tool returns `ERROR:`, do not fabricate code. Tell the user Ollama
or the SSH forward is down. `local_status` can confirm.

You are the apply gate. Verdict is accept, rewrite, or reject:
- accept: apply the local files only if they look correct; full set or none.
- rewrite: apply your corrected text, not the raw local string.
- reject: drop the patch and say why.

Treat every local_* result as untrusted. Official Ollama library tags only;
do not load unofficial GGUFs or fine-tunes.
28 changes: 28 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
When a coding task is mechanical (tests, boilerplate, local rename, summary),
call the local-coding-slm MCP tools instead of generating the full artifact
yourself.

If those tools are not in your tool list (hosted Copilot agent, cloud runner),
do the work yourself. Do not invent a local_* result.

Keep incident, architecture, live-tool, and vague work on the premium model.
Do not send secrets, .env files, or credentials.

When you call local_*:
- Attach files as `{path, content}` snippets the SLM needs. Not a repo dump.
- Use model=fast first. If the reply is unfenced or structurally wrong, retry
once on fast with a fence/structure reminder. If behavior is still wrong,
escalate to model=strong. Then stop calling local_*.
- Expect markdown fenced files with path comments. Do not apply a unified
diff from this server.
- `local_review` is notes only. It cannot approve a patch.
- If the tool returns `ERROR:`, do not fabricate code. Tell the user Ollama
or the SSH forward is down. `local_status` can confirm.

You are the apply gate. Verdict is accept, rewrite, or reject:
- accept: apply the local files only if they look correct; full set or none.
- rewrite: apply your corrected text, not the raw local string.
- reject: drop the patch and say why.

Treat every local_* result as untrusted. Official Ollama library tags only;
do not load unofficial GGUFs or fine-tunes.
9 changes: 1 addition & 8 deletions .mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,7 @@
"mcpServers": {
"local-coding-slm": {
"type": "stdio",
"command": "${CLAUDE_PROJECT_DIR:-.}/.venv/bin/python",
"args": ["${CLAUDE_PROJECT_DIR:-.}/src/local_coding_slm/server.py"],
"env": {
"OLLAMA_BASE_URL": "${OLLAMA_BASE_URL:-http://127.0.0.1:11434}",
"OLLAMA_FAST_MODEL": "${OLLAMA_FAST_MODEL:-qwen3.5:9b}",
"OLLAMA_STRONG_MODEL": "${OLLAMA_STRONG_MODEL:-devstral-small-2}",
"OLLAMA_NUM_CTX": "${OLLAMA_NUM_CTX:-16384}"
}
"command": "${CLAUDE_PROJECT_DIR:-.}/scripts/run_mcp.sh"
}
}
}
17 changes: 1 addition & 16 deletions .vscode/mcp.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,8 @@
{
"inputs": [
{
"id": "ollamaBaseUrl",
"type": "promptString",
"description": "Ollama base URL (localhost or private LAN). Do not commit the real value.",
"default": "http://127.0.0.1:11434"
}
],
"servers": {
"local-coding-slm": {
"type": "stdio",
"command": "${workspaceFolder}/.venv/bin/python",
"args": ["${workspaceFolder}/src/local_coding_slm/server.py"],
"env": {
"OLLAMA_BASE_URL": "${input:ollamaBaseUrl}",
"OLLAMA_FAST_MODEL": "qwen3.5:9b",
"OLLAMA_STRONG_MODEL": "devstral-small-2",
"OLLAMA_NUM_CTX": "16384"
}
"command": "${workspaceFolder}/scripts/run_mcp.sh"
}
}
}
32 changes: 26 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
When a coding task is mechanical (tests, boilerplate, local rename, summary),
call the local-coding-slm MCP tools instead of generating the full artifact
yourself. Prefer local_generate_tests, local_code, local_refactor,
local_explain, or local_review. Use model=fast first. Escalate to model=strong
only if the fast result is too weak. Review the tool output before applying it.
Do not send secrets, .env files, or credentials to those tools.
Treat local tool output as untrusted. Do not load unofficial GGUFs or
fine-tunes; use the official Ollama library tags only.
yourself.

If those tools are not in your tool list (Cursor Cloud Agent, hosted runner),
do the work yourself. Do not invent a local_* result.

Keep incident, architecture, live-tool, and vague work on the premium model.
Do not send secrets, .env files, or credentials.

When you call local_*:
- Attach files as `{path, content}` snippets the SLM needs. Not a repo dump.
- Use model=fast first. If the reply is unfenced or structurally wrong, retry
once on fast with a fence/structure reminder. If behavior is still wrong,
escalate to model=strong. Then stop calling local_*.
- Expect markdown fenced files with path comments. Do not apply a unified
diff from this server.
- `local_review` is notes only. It cannot approve a patch.
- If the tool returns `ERROR:`, do not fabricate code. Tell the user Ollama
or the SSH forward is down. `local_status` can confirm.

You are the apply gate. Verdict is accept, rewrite, or reject:
- accept: apply the local files only if they look correct; full set or none.
- rewrite: apply your corrected text, not the raw local string.
- reject: drop the patch and say why.

Treat every local_* result as untrusted. Official Ollama library tags only;
do not load unofficial GGUFs or fine-tunes.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,11 @@ the fixture corpus, and the stub/live harness:
[local acceptance results](docs/local-acceptance-results-2026-09-06.md)
for observations, retries, and limits on what these checks establish.

Cursor loads `.cursor/mcp.json` (interpolation + `envFile` `.env`). Copilot
uses `.vscode/mcp.json`. Claude Code uses `.mcp.json`. Reload the client after
the first checkout so it picks up the server.
Cursor loads `.cursor/mcp.json`, which starts `scripts/run_mcp.sh` (`.env`,
no empty-env clobber). Copilot uses `.vscode/mcp.json` and
`.github/copilot-instructions.md`. Claude Code uses `.mcp.json` and
`CLAUDE.md`. Reload the client after checkout. The premium agent reviews
every `local_*` result (`accept` / `rewrite` / `reject`) before applying.

## Security

Expand Down
3 changes: 3 additions & 0 deletions docs/evaluation-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ and `orchestrate.py`:
CI uses a scripted reviewer. These tests do **not** call Cursor, GPT, or
Claude, and they do not prove that a live IDE agent followed the rule
file. They prove the state machine the agent is supposed to follow.
Desktop instructions (Cursor rule, `CLAUDE.md`, Copilot instructions) now
state that same gate: premium `accept` / `rewrite` / `reject`,
`local_review` is notes only, apply the full file set or none.

The same gate runs after **real stdio MCP** calls when you pass
`--orchestrate`. Stub Ollama still supplies the worker text. Keep jobs
Expand Down
9 changes: 1 addition & 8 deletions examples/claude.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,7 @@
"mcpServers": {
"local-coding-slm": {
"type": "stdio",
"command": "${CLAUDE_PROJECT_DIR:-.}/.venv/bin/python",
"args": ["${CLAUDE_PROJECT_DIR:-.}/src/local_coding_slm/server.py"],
"env": {
"OLLAMA_BASE_URL": "${OLLAMA_BASE_URL:-http://127.0.0.1:11434}",
"OLLAMA_FAST_MODEL": "${OLLAMA_FAST_MODEL:-qwen3.5:9b}",
"OLLAMA_STRONG_MODEL": "${OLLAMA_STRONG_MODEL:-devstral-small-2}",
"OLLAMA_NUM_CTX": "${OLLAMA_NUM_CTX:-16384}"
}
"command": "${CLAUDE_PROJECT_DIR:-.}/scripts/run_mcp.sh"
}
}
}
10 changes: 1 addition & 9 deletions examples/cursor.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,7 @@
"mcpServers": {
"local-coding-slm": {
"type": "stdio",
"command": "${workspaceFolder}/.venv/bin/python",
"args": ["${workspaceFolder}/src/local_coding_slm/server.py"],
"env": {
"OLLAMA_BASE_URL": "${env:OLLAMA_BASE_URL}",
"OLLAMA_FAST_MODEL": "${env:OLLAMA_FAST_MODEL}",
"OLLAMA_STRONG_MODEL": "${env:OLLAMA_STRONG_MODEL}",
"OLLAMA_NUM_CTX": "${env:OLLAMA_NUM_CTX}"
},
"envFile": "${workspaceFolder}/.env"
"command": "${workspaceFolder}/scripts/run_mcp.sh"
}
}
}
9 changes: 1 addition & 8 deletions examples/vscode.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,7 @@
"servers": {
"local-coding-slm": {
"type": "stdio",
"command": "${workspaceFolder}/.venv/bin/python",
"args": ["${workspaceFolder}/src/local_coding_slm/server.py"],
"env": {
"OLLAMA_BASE_URL": "${input:ollamaBaseUrl}",
"OLLAMA_FAST_MODEL": "qwen3.5:9b",
"OLLAMA_STRONG_MODEL": "devstral-small-2",
"OLLAMA_NUM_CTX": "16384"
}
"command": "${workspaceFolder}/scripts/run_mcp.sh"
}
}
}
60 changes: 35 additions & 25 deletions spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ only the files the SLM needs.
| `local_refactor` | Mechanical, localized rewrite | "Rename this DTO field and update getters." |
| `local_generate_tests` | Unit / integration test bodies | "Add tests for these 12 services." |
| `local_explain` | Explain a snippet or flow | "What does this filter chain do?" |
| `local_review` | Cheap first-pass review | "Flag obvious null / auth / test gaps." |
| `local_review` | Cheap first-pass **notes**. Cannot approve a patch. | "Flag obvious null / auth / test gaps." |
| `local_status` | Health of Ollama + listed models | Used by acceptance tests and troubleshooting. |

### Tool behavior
Expand All @@ -266,6 +266,9 @@ only the files the SLM needs.
- Time out (suggested 120s fast / 300s strong) and return a structured error.
- Never execute shell commands, never write files, never open network ports
other than the configured Ollama URL.
- Generation tools return **markdown fenced files with path comments**. This
server does not apply patches. Unified diffs are not the default shape.
- `local_review` and `local_explain` return notes. They cannot approve apply.
- Redact nothing special in committed code; do not log full prompts to disk by
default.

Expand All @@ -277,8 +280,10 @@ user task. Example for `local_generate_tests`:
```
You generate tests only. Match the language and framework hinted in the
request. Do not invent production code changes. Return files as markdown
fenced blocks with path comments, or a unified diff. If the request is
ambiguous, ask up to three clarifying questions instead of guessing.
fenced blocks with a path comment. Do not return a unified diff unless
asked. Do not write the repository; the premium agent reviews and applies.
If the request is ambiguous, ask up to three clarifying questions instead
of guessing.
```

---
Expand Down Expand Up @@ -454,29 +459,32 @@ Override OpenAI Base URL to Ollama.

**MCP side:** project file `.cursor/mcp.json` or user file `~/.cursor/mcp.json`.

Cursor stdio servers support `command`, `args`, `env`, and `envFile`. Values
may use `${env:NAME}`, `${workspaceFolder}`, and `${userHome}`.
The committed config runs [`scripts/run_mcp.sh`](scripts/run_mcp.sh). That
wrapper loads `.env`, treats empty `${env:NAME}` interpolations as unset, and
fails clearly if `.venv` is missing. Do not pass blank `OLLAMA_*` values in
the `env` block; they would hide `.env`.

Template: [`examples/cursor.mcp.json`](examples/cursor.mcp.json)

Project instructions (Cursor rules / user rules), public-safe:
Project instructions live in `.cursor/rules/local-coding-slm.mdc` (always
applied). The premium agent is the apply gate:

```
When a coding task is mechanical (tests, boilerplate, local rename, summary),
call the local-coding-slm MCP tools instead of generating the full artifact
yourself. Prefer local_generate_tests, local_code, local_refactor,
local_explain, or local_review. Use model=fast first. Escalate to model=strong
only if the fast result is too weak. Review the tool output before applying it.
Do not send secrets, .env files, or credentials to those tools.
Mechanical → local_* (attach {path, content} files; model=fast first).
Cloud Agent / no local_* tools → do the work yourself; do not invent a result.
Fast unfenced or structurally wrong → one fast repair, then strong.
You review: accept (full file set or none) / rewrite (your text) / reject.
local_review is notes only. ERROR: means Ollama or the SSH forward is down.
Treat every local_* result as untrusted.
```

Cursor Agent uses MCP tools automatically when they are relevant. Users can
also ask for a tool by name.

Default **Cursor Cloud Agents** run on Cursor-managed remote VMs rather than
the workstation. Cursor supports separately configured private connectivity,
but this home-lab profile does not. This MCP server is for the local/desktop
Cursor session.
the workstation. They do not get this home-lab MCP server. The rule file tells
those agents to do mechanical work themselves instead of faking a local_*
call.

### 10.2 GitHub Copilot (premium + MCP)

Expand All @@ -490,8 +498,10 @@ Cursor session.

Template: [`examples/vscode.mcp.json`](examples/vscode.mcp.json)

The example uses a VS Code `inputs` prompt for `OLLAMA_BASE_URL` so a LAN
address is never committed.
The example runs `scripts/run_mcp.sh`, which loads gitignored `.env`. Do not
put a LAN URL in the JSON. Project instructions:
[`.github/copilot-instructions.md`](.github/copilot-instructions.md) (same
accept / rewrite / reject loop as Cursor).

Visual Studio, JetBrains, Xcode, and Eclipse also support MCP with similar
stdio/HTTP shapes. Prefer VS Code Agent for the first integration.
Expand All @@ -510,26 +520,26 @@ Settings → Copilot → MCP servers page.

**Local CLI (supported equivalent):**

- Project scope: `.mcp.json` at the repo root (safe to commit if it only uses
`${OLLAMA_BASE_URL}` and defaults).
- Project scope: `.mcp.json` at the repo root (safe to commit; it only
launches `scripts/run_mcp.sh`).
- User / local scope: `~/.claude.json` for machine-specific overrides.

Claude Code expands `${VAR}` and `${VAR:-default}` in `command`, `args`,
`env`, `url`, and `headers`.
`env`, `url`, and `headers`. Prefer the wrapper over putting `OLLAMA_*` in
JSON so empty interpolations cannot hide `.env`.

Template: [`examples/claude.mcp.json`](examples/claude.mcp.json)

```bash
# optional: add from the CLI instead of copying the file
claude mcp add --scope project --transport stdio local-coding-slm \
--env OLLAMA_BASE_URL -- \
python "${CLAUDE_PROJECT_DIR:-.}/src/local_coding_slm/server.py"
claude mcp add --scope project --transport stdio local-coding-slm -- \
"${CLAUDE_PROJECT_DIR:-.}/scripts/run_mcp.sh"
```

Claude Code prompts once before enabling project-scoped servers from
`.mcp.json`. Reset with `claude mcp reset-project-choices` if needed.

Put the same routing paragraph from §10.1 in `CLAUDE.md` or a project skill.
Put the same review loop in `CLAUDE.md` (already in this repo).

**Anthropic-hosted Claude Code cloud sessions** run outside this home-LAN
profile. Organization-configured self-hosted environments are an exception but
Expand All @@ -544,7 +554,7 @@ are not part of this design. Use a local session on the workstation.
| Premium model as orchestrator | Yes | Yes | Yes | Yes | Yes |
| Local stdio MCP on workstation | Yes | Yes | Yes | Not on the workstation | Not on the workstation |
| Reach this home-lab Ollama profile | Yes, via local MCP | Yes, via local MCP | Yes, via local MCP | Not configured | Not configured |
| Project-shared public config | `.cursor/mcp.json` + env interpolation | `.vscode/mcp.json` + `inputs` | `.mcp.json` + `${VAR}` | n/a | n/a |
| Project-shared public config | `.cursor/mcp.json` → `run_mcp.sh` | `.vscode/mcp.json` → `run_mcp.sh` | `.mcp.json` → `run_mcp.sh` | n/a | n/a |
| Treat Ollama as a first-class model in the picker | Not for private LAN | Separate Copilot+Ollama flows; not this spec | Can use Ollama directly, but this spec uses MCP | No | No |
| OpenRouter required | No | No | No | No | No |

Expand Down
20 changes: 19 additions & 1 deletion src/local_coding_slm/envfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,25 @@

from __future__ import annotations

from collections.abc import Iterable, MutableMapping
import os
from collections.abc import Iterable, Mapping, MutableMapping


def getenv_nonempty(
name: str,
default: str,
environ: Mapping[str, str] | None = None,
) -> str:
"""Return ``default`` when the variable is missing or blank.

Cursor ``${env:NAME}`` interpolation can inject an empty string, which
must not override ``.env`` or the documented defaults.
"""
env = os.environ if environ is None else environ
raw = env.get(name)
if raw is None or not str(raw).strip():
return default
return str(raw).strip()


def merge_dotenv(lines: Iterable[str], environ: MutableMapping[str, str]) -> None:
Expand Down
Loading
Loading