Skip to content

opal mcp exits instantly as stdio server — root-caused to risk-WIP ImportError, committed code passes #41

Description

@CST-100

Reported

opal mcp is unusable as a stdio MCP server. Two requirements stated:

  1. The process must block serving stdio until stdin closes. Observed (over ssh): startup banners print, then the prompt returns immediately. Acceptance: timeout 5 uv run opal mcp >/dev/null; echo $? prints 124, not 0/1 instantly.
  2. All human-readable output (banners, project/database lines, logging) goes to stderr; stdout carries JSON-RPC exclusively from byte zero. Acceptance: uv run opal mcp 1>/dev/null still shows the banner; 2>/dev/null shows nothing.

Investigation (2026-06-11, commit 13cdbc6; src/opal/mcp/server.py and src/opal/__main__.py are identical on devel and master)

Committed code passes both acceptance criteria on the reporter's machine:

Test stdin Result
timeout 8 opal mcp >/dev/null held open (pipe) exit 124, stdout 0 bytes, banners on stderr
timeout 8 opal mcp under a pty (script -qec) TTY exit 124, stdout 0 bytes
timeout 8 opal mcp --project /home/lv154/code/opal (Purple Orchid) held open exit 124, stdout 0 bytes, all banners incl. OPAL MCP server | project: Purple Orchid on stderr
timeout 10 opal mcp < /dev/null EOF exit 0 immediately — correct stdio behavior (stdin already closed)

Entry-point prints were moved to stderr in aa3d366 ("Every entry point states which database it resolved, and why"); run_server (src/opal/mcp/server.py:5002) banners likewise target stderr, and the stdio loop (async with stdio_server(): await server.run(...)) is awaited correctly. Note: the server is the low-level mcp.server.Server, not FastMCP. Versions: mcp 1.27.1, anyio 4.12.0.

Root cause of the observed failure

The instant exit reproduces only in the main checkout /home/lv154/code/opal, branch feature/risk-scenarios with uncommitted risk-module changes (#40). The rewritten src/opal/db/models/risk.py drops RiskStatus (replaced by RiskDisposition / RiskIssueRole), while src/opal/mcp/server.py:63 still does from opal.db.models.risk import RiskStatus:

Using project: Purple Orchid (/home/lv154/code/opal)
Database: sqlite:////home/lv154/code/opal/data/opal.db (opal.project.yaml auto-detected)
Traceback (most recent call last):
  ...
  File ".../src/opal/mcp/server.py", line 63, in <module>
    from opal.db.models.risk import RiskStatus
ImportError: cannot import name 'RiskStatus' from 'opal.db.models.risk'

Exit 1 immediately after the banner lines — matching the reported symptom. Blast radius of the WIP is wider than MCP: web/routes.py, api/routes/risks.py, and seed.py also import RiskStatus, so opal serve and opal seed are equally down on that checkout.

Disposition

Cannot duplicate on committed code — defect attributed to in-progress risk-module refactor (test article was the dirty feature/risk-scenarios working tree).

  • Corrective action: when the risk model lands, update the four RiskStatus importers (mcp/server.py, web/routes.py, api/routes/risks.py, seed.py) in the same change — scope of Development: Risks #40, not a separate fix.
  • Preventive action: add a stdio-contract regression test that spawns opal mcp as a subprocess and asserts (a) it stays alive while stdin is open, (b) stdout is byte-clean JSON-RPC through the initialize handshake, (c) it exits cleanly on stdin EOF.

Discrepancy / needed from reporter

The reported banner was OPAL MCP server | project: ..., which prints inside run_server — i.e. after the import that fails above. If that exact line really appeared before the prompt returned (rather than Using project: Purple Orchid + traceback), the failing run was a different environment than this checkout: please provide host, cwd, git rev-parse HEAD + git status -s, and the unredirected stderr of the failing run.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions