feat(mcp): configure OpenCode models per workflow

Add server-side model and variant settings for MCP ingest and query OpenCode jobs. Ingest defaults to openai/gpt-5.5 high, while query defaults to openai/gpt-5.5 minimal, and readiness/status surfaces the active settings.

Persist the settings through the managed service env file and preserve local overrides across start, tools, and token rotations. Remote MCP clients still cannot choose arbitrary models per call.

Validation: python3 -m unittest discover -v; python3 -m py_compile obsidian_wiki/mcp_ingest.py obsidian_wiki/mcp_query.py obsidian_wiki/mcp_server.py scripts/manage_mcp_service.py tests/test_mcp_ingest.py tests/test_mcp_query.py tests/test_mcp_server_remote.py tests/test_mcp_service.py; git diff --check.
This commit is contained in:
Codex
2026-07-05 18:20:06 +00:00
parent e7913d4c6b
commit 3d0203fb9e
11 changed files with 253 additions and 12 deletions
+56 -11
View File
@@ -32,6 +32,10 @@ DEFAULT_INGEST_UPLOAD_PATH = "/mcp-ingest/uploads"
DEFAULT_TOKEN_BYTES = 32
DEFAULT_UV_CACHE_DIR = "/tmp/uv-cache"
DEFAULT_OPENCODE_PATH = "opencode"
DEFAULT_INGEST_OPENCODE_MODEL = "openai/gpt-5.5"
DEFAULT_INGEST_OPENCODE_VARIANT = "high"
DEFAULT_QUERY_OPENCODE_MODEL = "openai/gpt-5.5"
DEFAULT_QUERY_OPENCODE_VARIANT = "minimal"
TOOL_FILE_ASSETS = "file-assets"
TOOL_INGEST = "ingest"
TOOL_QUERY = "query"
@@ -115,6 +119,14 @@ class ServicePaths:
env_path: Path
@dataclass(frozen=True)
class OpenCodeModelSettings:
ingest_model: str = DEFAULT_INGEST_OPENCODE_MODEL
ingest_variant: str = DEFAULT_INGEST_OPENCODE_VARIANT
query_model: str = DEFAULT_QUERY_OPENCODE_MODEL
query_variant: str = DEFAULT_QUERY_OPENCODE_VARIANT
def generate_token(num_bytes: int = DEFAULT_TOKEN_BYTES) -> str:
return secrets.token_urlsafe(num_bytes)
@@ -248,10 +260,12 @@ def write_token_env_file(
enable_ingest: bool | None = None,
enable_query: bool | None = None,
opencode_path: str | Path | None = None,
model_settings: OpenCodeModelSettings | None = None,
) -> None:
tools = normalize_enabled_tools(DEFAULT_ENABLED_TOOLS if enabled_tools is None else enabled_tools)
tools = update_ingest_tool(tools, enable_ingest)
tools = update_query_tool(tools, enable_query)
models = model_settings or OpenCodeModelSettings()
env_path.parent.mkdir(parents=True, exist_ok=True)
env_path.parent.chmod(0o700)
fd = os.open(env_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
@@ -262,6 +276,10 @@ def write_token_env_file(
env_file.write(f"OBSIDIAN_MCP_PATH={path}\n")
env_file.write(f"OBSIDIAN_MCP_ENABLED_TOOLS={format_enabled_tools(tools)}\n")
env_file.write(f"OBSIDIAN_MCP_ENABLE_INGEST={1 if TOOL_INGEST in tools else 0}\n")
env_file.write(f"OBSIDIAN_MCP_INGEST_OPENCODE_MODEL={models.ingest_model}\n")
env_file.write(f"OBSIDIAN_MCP_INGEST_OPENCODE_VARIANT={models.ingest_variant}\n")
env_file.write(f"OBSIDIAN_MCP_QUERY_OPENCODE_MODEL={models.query_model}\n")
env_file.write(f"OBSIDIAN_MCP_QUERY_OPENCODE_VARIANT={models.query_variant}\n")
if opencode_path:
env_file.write(f"OBSIDIAN_MCP_OPENCODE_PATH={opencode_path}\n")
if TOOL_INGEST in tools:
@@ -284,7 +302,20 @@ def read_service_env_file(env_path: Path) -> dict[str, str]:
return values
def saved_service_config(project_root: Path) -> tuple[str, str, int, str, tuple[str, ...], str]:
def model_settings_from_config(values: dict[str, str]) -> OpenCodeModelSettings:
return OpenCodeModelSettings(
ingest_model=values.get("OBSIDIAN_MCP_INGEST_OPENCODE_MODEL", DEFAULT_INGEST_OPENCODE_MODEL).strip()
or DEFAULT_INGEST_OPENCODE_MODEL,
ingest_variant=values.get("OBSIDIAN_MCP_INGEST_OPENCODE_VARIANT", DEFAULT_INGEST_OPENCODE_VARIANT).strip()
or DEFAULT_INGEST_OPENCODE_VARIANT,
query_model=values.get("OBSIDIAN_MCP_QUERY_OPENCODE_MODEL", DEFAULT_QUERY_OPENCODE_MODEL).strip()
or DEFAULT_QUERY_OPENCODE_MODEL,
query_variant=values.get("OBSIDIAN_MCP_QUERY_OPENCODE_VARIANT", DEFAULT_QUERY_OPENCODE_VARIANT).strip()
or DEFAULT_QUERY_OPENCODE_VARIANT,
)
def saved_service_config(project_root: Path) -> tuple[str, str, int, str, tuple[str, ...], str, OpenCodeModelSettings]:
paths = resolve_service_paths(project_root)
values = read_service_env_file(paths.env_path)
token = values.get("OBSIDIAN_MCP_TOKEN", "").strip()
@@ -298,7 +329,7 @@ def saved_service_config(project_root: Path) -> tuple[str, str, int, str, tuple[
values.get("OBSIDIAN_MCP_OPENCODE_PATH", "").strip()
or values.get("OBSIDIAN_MCP_INGEST_OPENCODE_PATH", "").strip()
)
return token, host, port, path, enabled_tools, opencode_path
return token, host, port, path, enabled_tools, opencode_path, model_settings_from_config(values)
def write_service_unit(
@@ -420,7 +451,7 @@ def render_remote_config(
def show_config(project_root: Path) -> str:
paths = resolve_service_paths(project_root)
token, _host, port, path, enabled_tools, _opencode_path = saved_service_config(project_root)
token, _host, port, path, enabled_tools, _opencode_path, _models = saved_service_config(project_root)
return render_remote_config(
hosts=detect_remote_hosts(),
token=token,
@@ -459,8 +490,9 @@ def new_token(
paths = resolve_service_paths(project_root)
enabled_tools = DEFAULT_ENABLED_TOOLS
opencode_path = ""
model_settings = OpenCodeModelSettings()
try:
_old_token, _old_host, _old_port, _old_path, enabled_tools, opencode_path = saved_service_config(project_root)
_old_token, _old_host, _old_port, _old_path, enabled_tools, opencode_path, model_settings = saved_service_config(project_root)
except RuntimeError:
pass
token = generate_token(token_bytes)
@@ -472,6 +504,7 @@ def new_token(
path=path,
enabled_tools=enabled_tools,
opencode_path=opencode_path,
model_settings=model_settings,
)
restart_service_if_installed(paths)
return render_remote_config(
@@ -497,13 +530,14 @@ def start_service(
) -> str:
paths = resolve_service_paths(project_root)
try:
token, host, port, path, enabled_tools, opencode_path = saved_service_config(project_root)
token, host, port, path, enabled_tools, opencode_path, model_settings = saved_service_config(project_root)
except RuntimeError:
if paths.env_path.exists():
raise
token = generate_token(token_bytes)
enabled_tools = DEFAULT_ENABLED_TOOLS
opencode_path = ""
model_settings = OpenCodeModelSettings()
enabled_tools = update_ingest_tool(enabled_tools, enable_ingest)
enabled_tools = update_query_tool(enabled_tools, enable_query)
if ({TOOL_INGEST, TOOL_QUERY} & set(enabled_tools)) and not opencode_path:
@@ -516,6 +550,7 @@ def start_service(
path=path,
enabled_tools=enabled_tools,
opencode_path=opencode_path,
model_settings=model_settings,
)
uv_path = find_uv_path()
prepare_runtime(paths.project_root, uv_path=uv_path)
@@ -560,10 +595,10 @@ def render_tool_status(
return "\n".join(lines) + "\n"
def ensure_service_config(project_root: Path) -> tuple[ServicePaths, str, str, int, str, tuple[str, ...], str]:
def ensure_service_config(project_root: Path) -> tuple[ServicePaths, str, str, int, str, tuple[str, ...], str, OpenCodeModelSettings]:
paths = resolve_service_paths(project_root)
try:
token, host, port, path, enabled_tools, opencode_path = saved_service_config(project_root)
token, host, port, path, enabled_tools, opencode_path, model_settings = saved_service_config(project_root)
except RuntimeError:
token = generate_token()
host = DEFAULT_HOST
@@ -571,18 +606,27 @@ def ensure_service_config(project_root: Path) -> tuple[ServicePaths, str, str, i
path = DEFAULT_PATH
enabled_tools = DEFAULT_ENABLED_TOOLS
opencode_path = ""
write_token_env_file(paths.env_path, token, host=host, port=port, path=path, enabled_tools=enabled_tools)
return paths, token, host, port, path, enabled_tools, opencode_path
model_settings = OpenCodeModelSettings()
write_token_env_file(
paths.env_path,
token,
host=host,
port=port,
path=path,
enabled_tools=enabled_tools,
model_settings=model_settings,
)
return paths, token, host, port, path, enabled_tools, opencode_path, model_settings
def list_tools(project_root: Path) -> str:
paths, _token, _host, _port, _path, enabled_tools, _opencode_path = ensure_service_config(project_root)
paths, _token, _host, _port, _path, enabled_tools, _opencode_path, _models = ensure_service_config(project_root)
return render_tool_status(config_path=paths.env_path, enabled_tools=enabled_tools)
def set_tool_enabled(project_root: Path, tool: str, enabled: bool) -> str:
tool_name = normalize_tool_name(tool)
paths, token, host, port, path, enabled_tools, opencode_path = ensure_service_config(project_root)
paths, token, host, port, path, enabled_tools, opencode_path, model_settings = ensure_service_config(project_root)
current = set(enabled_tools)
if enabled:
current.add(tool_name)
@@ -599,6 +643,7 @@ def set_tool_enabled(project_root: Path, tool: str, enabled: bool) -> str:
path=path,
enabled_tools=updated_tools,
opencode_path=opencode_path,
model_settings=model_settings,
)
if paths.unit_path.exists() and {TOOL_INGEST, TOOL_QUERY} & set(updated_tools):
import_proxy_environment()