feat(settings): add GET /api/settings/mcp/env endpoint
Adds four helper functions (_infisical_client, _infisical_ctx, _read_infisical_values, _build_env_var_row) and the /api/settings/mcp/env endpoint that compares Infisical vs container env vars and reports drift. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
109
web/app.py
109
web/app.py
@@ -35,6 +35,13 @@ from legal_mcp.tools import cases as cases_tools, search as search_tools, workfl
|
||||
_web_dir = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(_web_dir.parent))
|
||||
from web.gitea_client import commit_and_push, create_repo, setup_remote_and_push
|
||||
from web.mcp_env_catalog import (
|
||||
ENV_CATALOG,
|
||||
EnvSpec,
|
||||
coerce,
|
||||
mask_secret,
|
||||
normalize_for_compare,
|
||||
)
|
||||
from web.progress_store import ProgressStore
|
||||
from web.paperclip_client import (
|
||||
archive_project as pc_archive_project,
|
||||
@@ -2543,6 +2550,108 @@ async def api_post_agent_comment(case_number: str, req: AgentCommentRequest):
|
||||
return result
|
||||
|
||||
|
||||
# ── Settings: MCP Server Configuration ────────────────────────────
|
||||
|
||||
|
||||
def _infisical_client():
|
||||
"""Build Infisical SDK client, or return None if not configured."""
|
||||
token = os.environ.get("INFISICAL_TOKEN", "")
|
||||
if not token:
|
||||
return None
|
||||
try:
|
||||
from infisical_sdk import InfisicalSDKClient
|
||||
return InfisicalSDKClient(token=token)
|
||||
except Exception as e:
|
||||
logger.warning("infisical_client_unavailable: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def _infisical_ctx():
|
||||
"""Return (project_id, environment, secret_path) for legal-ai secrets."""
|
||||
return (
|
||||
os.environ.get("INFISICAL_PROJECT_ID", "9a77b161-f70c-4dd3-9d67-b7ab850cef51"),
|
||||
os.environ.get("INFISICAL_ENV", "dev"),
|
||||
os.environ.get("INFISICAL_PATH", "/legal-ai"),
|
||||
)
|
||||
|
||||
|
||||
def _read_infisical_values() -> tuple[dict[str, str], list[str]]:
|
||||
"""Read all known catalog keys from Infisical. Returns (values, errors)."""
|
||||
client = _infisical_client()
|
||||
if client is None:
|
||||
return {}, ["infisical_unreachable"]
|
||||
project_id, env, path = _infisical_ctx()
|
||||
try:
|
||||
secrets = client.get_all_secrets(
|
||||
environment=env, project_id=project_id, secret_path=path
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("infisical_read_failed: %s", e)
|
||||
return {}, [f"infisical_read_failed: {e}"]
|
||||
values: dict[str, str] = {}
|
||||
for s in secrets:
|
||||
if s.secret_key in ENV_CATALOG:
|
||||
values[s.secret_key] = s.secret_value
|
||||
return values, []
|
||||
|
||||
|
||||
def _build_env_var_row(
|
||||
spec: EnvSpec,
|
||||
infisical_value: str | None,
|
||||
container_value: str | None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build a single response row for an env var."""
|
||||
if spec.is_secret:
|
||||
i_norm = mask_secret(infisical_value) if infisical_value else None
|
||||
c_norm = mask_secret(container_value) if container_value else None
|
||||
# drift: compare raw before masking
|
||||
drift = (
|
||||
(infisical_value or "") != (container_value or "")
|
||||
and bool(infisical_value or container_value)
|
||||
)
|
||||
infisical_display: str | None = i_norm
|
||||
container_display: str | None = c_norm
|
||||
else:
|
||||
infisical_display = infisical_value
|
||||
container_display = container_value
|
||||
drift = (
|
||||
normalize_for_compare(spec, infisical_value)
|
||||
!= normalize_for_compare(spec, container_value)
|
||||
)
|
||||
# only count as drift if at least one side is non-null
|
||||
if infisical_value is None and container_value is None:
|
||||
drift = False
|
||||
row = spec.to_public_dict()
|
||||
row.update({
|
||||
"infisical_value": infisical_display,
|
||||
"container_value": container_display,
|
||||
"drift": drift,
|
||||
})
|
||||
return row
|
||||
|
||||
|
||||
@app.get("/api/settings/mcp/env")
|
||||
async def api_mcp_env():
|
||||
"""List all catalog env vars with Infisical + container values."""
|
||||
infisical_values, errors = _read_infisical_values()
|
||||
project_id, env, path = _infisical_ctx()
|
||||
rows = []
|
||||
for key, spec in ENV_CATALOG.items():
|
||||
i_val = infisical_values.get(key)
|
||||
c_val = os.environ.get(key)
|
||||
rows.append(_build_env_var_row(spec, i_val, c_val))
|
||||
return {
|
||||
"vars": rows,
|
||||
"infisical_environment": env,
|
||||
"infisical_project_id": project_id,
|
||||
"infisical_path": path,
|
||||
"coolify_app_uuid": os.environ.get(
|
||||
"COOLIFY_APP_UUID", "gyjo0mtw2c42ej3xxvbz8zio"
|
||||
),
|
||||
"errors": errors,
|
||||
}
|
||||
|
||||
|
||||
# ── Settings: Tag → Company Mappings ──────────────────────────────
|
||||
|
||||
@app.get("/api/settings/paperclip-companies")
|
||||
|
||||
Reference in New Issue
Block a user