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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user