Files
opencode-devbox/rootfs/usr/local/lib/opencode-devbox/generate-config.py
T
Joakim Persson 3d4e739529
Validate / docs-check (push) Successful in 18s
Validate / validate-base (push) Failing after 11m26s
Validate / validate-omos (push) Failing after 13m14s
Add Context7 remote MCP server to auto-generated config
Context7 provides up-to-date library documentation for LLMs via a
remote endpoint — no local binary needed. Always registered since it
has no PATH dependency.

Also switches generated config from .json to .jsonc so we can include
a comment about the optional API key for higher rate limits. The
existing-config check now detects both file extensions.
2026-05-02 21:24:04 +00:00

184 lines
5.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Generate opencode.json from environment variables on first container start.
Safety guarantees:
- NEVER overwrites an existing opencode.json. If the file is present
(whether bind-mounted from the host, persisted in a named volume, or
previously generated), this script exits immediately without writing.
- Requires OPENCODE_PROVIDER to be set. Without it, no file is written.
Environment variables:
OPENCODE_PROVIDER Required. One of: anthropic, openai, amazon-bedrock.
OPENCODE_MODEL Optional. Overrides the provider default model.
AWS_REGION Bedrock only. Default: us-east-1.
AWS_PROFILE Bedrock only. Default: default.
MCP servers are auto-registered for tools detected on PATH:
- mempalace (if installed) — enabled
- gitea-mcp (if installed) — registered but disabled by default
Output path: $HOME/.config/opencode/opencode.json
"""
from __future__ import annotations
import json
import os
import shutil
import sys
from pathlib import Path
# Default model per provider. Update here when upstream changes.
DEFAULT_MODELS: dict[str, str] = {
"anthropic": "anthropic/claude-sonnet-4-6",
"openai": "openai/gpt-5.4",
"amazon-bedrock": (
"amazon-bedrock/global.anthropic.claude-sonnet-4-5-20250929-v1:0"
),
}
# Fallback when OPENCODE_PROVIDER is set but not recognized.
FALLBACK_MODEL = DEFAULT_MODELS["anthropic"]
SCHEMA_URL = "https://opencode.ai/config.json"
def build_config(provider: str, model: str) -> dict:
"""Build the base opencode.json structure for a provider."""
config: dict = {
"$schema": SCHEMA_URL,
"model": model,
"share": "disabled",
"autoupdate": False,
}
if provider == "amazon-bedrock":
config["provider"] = {
"amazon-bedrock": {
"options": {
"region": os.environ.get("AWS_REGION", "us-east-1"),
"profile": os.environ.get("AWS_PROFILE", "default"),
}
}
}
return config
def register_mcp_servers(config: dict) -> list[str]:
"""Auto-register MCP servers for tools detected on PATH.
Returns the list of server names that were added. The "mcp" key
is only added to the config when at least one server is registered.
"""
servers: dict[str, dict] = {}
# MemPalace — local-first AI memory (if installed).
# `mempalace-mcp` is the entry-point binary shipped by the mempalace
# Python package. `uv tool install mempalace` places it on PATH as a
# shim whose shebang points at the isolated venv's Python, so system
# `python3 -m mempalace.mcp_server` (which would fail — system
# python3 can't import from the uv venv) is unnecessary here.
if shutil.which("mempalace-mcp"):
servers["mempalace"] = {
"type": "local",
"command": ["mempalace-mcp"],
}
# Gitea — self-hosted Git forge API (if installed).
# Disabled by default; user must set GITEA_ACCESS_TOKEN + GITEA_HOST
# and flip enabled=true in their config.
if shutil.which("gitea-mcp"):
servers["gitea"] = {
"type": "local",
"command": ["gitea-mcp", "-t", "stdio"],
"enabled": False,
}
# Context7 — up-to-date library documentation for LLMs (remote).
# Free tier works without an API key; set CONTEXT7_API_KEY for higher
# rate limits. No local binary needed — purely a remote MCP endpoint.
servers["context7"] = {
"type": "remote",
"url": "https://mcp.context7.com/mcp",
}
if servers:
config["mcp"] = servers
return list(servers.keys())
def main() -> int:
provider = os.environ.get("OPENCODE_PROVIDER", "").strip()
if not provider:
# No provider set — nothing to do. Not an error.
return 0
home = Path(os.environ.get("HOME", "/home/developer"))
config_dir = home / ".config" / "opencode"
config_file = config_dir / "opencode.jsonc"
config_file_legacy = config_dir / "opencode.json"
# CRITICAL: never overwrite an existing config. Users may have
# bind-mounted their host config directory, or their config may be
# persisted in a named volume from a previous run.
# Check both .json and .jsonc variants.
if config_file.exists() or config_file_legacy.exists():
existing = config_file if config_file.exists() else config_file_legacy
print(
f"Existing config found at {existing}"
"skipping generation.",
file=sys.stderr,
)
return 0
if provider not in DEFAULT_MODELS:
print(
f"WARNING: unknown OPENCODE_PROVIDER={provider!r}, "
f"falling back to default model {FALLBACK_MODEL!r}.",
file=sys.stderr,
)
model = os.environ.get("OPENCODE_MODEL", "").strip() or DEFAULT_MODELS.get(
provider, FALLBACK_MODEL
)
print(f"Generating opencode config for provider: {provider}", file=sys.stderr)
config = build_config(provider, model)
added = register_mcp_servers(config)
config_dir.mkdir(parents=True, exist_ok=True)
# Write as JSONC so we can include helpful comments.
content = json.dumps(config, indent=2)
# Insert a comment about Context7 API key after the context7 url line.
context7_comment = (
' "url": "https://mcp.context7.com/mcp"\n'
" // For higher rate limits, sign up at https://context7.com/dashboard\n"
' // and add: "headers": { "CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}" }'
)
content = content.replace(
' "url": "https://mcp.context7.com/mcp"',
context7_comment,
)
with config_file.open("w") as f:
f.write(content)
f.write("\n")
if added:
print(
f"MCP servers registered in opencode config: {', '.join(added)}.",
file=sys.stderr,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())