Source: https://oliviercouthaud.com/healcontrol · Status: Prototype · 2026 · Tags: Python, MCP, watsonx Repository or link: https://github.com/hl9082/healcontrol # HealControl A server that lets an AI assistant run a failing test suite, patch the bug, check the fix and open a pull request. HealControl is a Model Context Protocol server I wrote on 30 and 31 January 2026 for an IBM hackathon, in a team of four. It was another hackathon for me, and I had fun taking part and working with my teammates. The loop runs end to end on the demo app, and I recorded a video of it for the hackathon and posted it on LinkedIn. We didn't win. The latest version is on the dev branch of the repo. It is a demo, not a product. ## What I wanted I wanted the assistant to work like an on-call site reliability engineer: run the tests, read the file, change it, run the tests again, and only then commit. Each step is a tool exposed over MCP, so any client can drive the loop itself. The server stays thin: each tool does one thing and returns plain text, and the agent does the thinking. **What is MCP?** The Model Context Protocol is a standard way for an AI application to call tools that live somewhere else. A server announces a list of tools with names, arguments and descriptions, and the client's model decides when to call them. It means one server can work with several assistants, for example IBM Bob or Claude Desktop, without a separate integration for each. ## How it works One Python file, src/server.py, runs a FastMCP server over stdio and registers 12 tools. Five of them run a subprocess (pytest, git or the GitHub CLI), four read or write files in the app folder, two handle configuration, and analyze_with_watsonx is a REST call. A small healcontrol.json lists the apps the server may work on and which one is active. Only the demo app is in it. **What does stdio mean here?** The client starts the server as a child process and the two talk through its standard input and output, as lines of JSON. There is no network port and no login. That is why setup is one line in a VS Code config file, and also why the server runs with the same permissions as whoever launched it. Figure: Diagram. The server is one Python process that spawns pytest, git and gh as subprocesses; the agent in the client decides the order of calls. Orange is the call that sends the test output to IBM; the push to GitHub goes out through git and gh. **What the 12 tools touch.** | Kind | Tools | |---|---| | Run a subprocess (5) | check_pipeline_status, verify_fix, create_git_branch, commit_fix, push_to_cloud | | Read or write files (4) | list_files, read_code_file, apply_surgical_fix, reset_broken_app | | Configuration (2) | list_apps, set_active_app | | Call IBM over REST (1) | analyze_with_watsonx | On the dev branch the prompt asks the agent to detect, read, diagnose with Granite, fix, verify, then branch, commit and push. It also tells it to speak like a senior SRE, to be brief, not to ask permission, and never to modify test files. main still has an older prompt without the diagnose and push steps. **What are IBM watsonx and Granite?** Granite is IBM's family of language models, and watsonx.ai is IBM's hosted service for running them. The server fetches a token with an API key, caches it for the life of the process, and posts the failing test output to the chat endpoint, asking for four labelled fields: root cause, fix suggestion, confidence and explanation. It uses granite-4-h-small with a temperature of 0.2 and a 500 token cap. ## The loop, step by step The server doesn't run the loop. The agent does, using the order in the prompt, and it can skip or repeat steps. Here is the order the prompt gives it, with what each tool really returns. ### Step through the fix loop **1 Detect, check_pipeline_status().** Runs pytest on the active app and returns "Pipeline status: FAILED" followed by only the FAILURES section of the output. On the demo app, all three tests fail. If they pass it returns the whole output instead. Figure: Diagram. Step order is the one in the system prompt on dev. The agent chooses its own calls. Click a box or a button, or use Back and Next. This is what the code does, not a recording of a run. ## The demo bug The demo app is one function. calculate_discount subtracts the percentage from the price instead of taking a percentage of it. Three tests define correct behaviour, and all three fail on the buggy version. The fix is a single line. **The demo's three tests. "Buggy" is price - discount_percent, worked out by hand. "Fixed" is price - (price * discount_percent / 100).** | Test | Expected | Buggy returns | Fixed returns | |---|---|---|---| | 200 at 20% | 160 | 180 | 160 | | 50 at 10% | 45 | 40 | 45 | | 75 at 100% | 0 | -25 | 0 | ### Run the bug yourself Plain arithmetic in your browser, using the two versions of the function above. Drag the sliders or press a test button. Needs JavaScript; the table above has the same numbers. ## What I ran into **The watsonx SDK.** The first version used IBM's Python SDK with Granite 3.3. It was slow to initialise, so I switched to plain REST calls with a cached IAM token and moved to Granite 4. **Git in the wrong folder.** Branch, commit and push were running from the demo app's folder instead of the project root. I moved all of them to the project root on the last evening. **The "local" claim.** I described it as running locally with no code leaving the machine except the push. That holds only while analyze_with_watsonx isn't used: when it is, it sends the pytest output, tracebacks included, to IBM. And whatever model runs the client sees every tool result anyway. Figure: Diagram. What leaves the machine. IBM gets the test output each time the agent asks for a diagnosis, GitHub gets the branch on a push, and whoever runs the client's model sees what the tools return, so that part depends on the client. **Rules only in the prompt.** The prompt says never edit tests, but apply_surgical_fix will overwrite any file in the app folder, tests included, and commit_fix stages everything. The one workflow rule the code enforces is in push_to_cloud: test_result = _run(_build_test_cmd(), cwd=PROJECT_ROOT) if test_result.returncode != 0: return ( "Refusing to push: tests are failing. Fix the tests first.\n\n" + _extract_failures(test_result.stdout + test_result.stderr) ) **What does the path check do?** Every file name the agent passes is joined onto the app folder, resolved to an absolute path, and rejected if it ends up outside that folder, for instance with ../. Without it, an agent, or a prompt injected into a file it reads, could read or overwrite anything the server's user can. It stops that. It doesn't stop the agent from editing a test file inside the folder. **Emergency mode.** For demo day we planned a fallback: if GitHub went down, the server would use a fake failure log in src/mock_data.py. The fallback log isn't wired in yet: the mock failure data sits unused on the main branch, so it is a plan, not a feature. ## Where it stands It targets the demo app. The multi-app config exists with only the demo app in it, and the server has no tests of its own yet. The record of a full run is the video from the hackathon; there is no success rate and no log of what Granite replied. The agent acts without asking for approval, which suits a toy bug and not real code. Next come an approval step before any write, and a lock on test files that lives in the code instead of the prompt. - **Stack**: Python 3.12, FastMCP (stdio), pytest, git and gh CLI, IBM watsonx.ai (Granite) over REST - **Status**: Prototype - **Scope**: Hackathon demo on one toy app. It runs end to end. - **Video**: Recorded for the hackathon and posted on LinkedIn - **Credit**: A team of four. I wrote the server and the agent prompt. - **Code**: [github.com/hl9082/healcontrol](https://github.com/hl9082/healcontrol) (a teammate's repo; the latest version is on the dev branch) [← All projects](https://oliviercouthaud.com/projects)