Olivier Couthaud

HealControl

A server that lets an AI assistant run a failing test suite, patch the bug, check the fix and open a pull request.

I made HealControl for an IBM hackathon on 30 and 31 January 2026, in a team of four. It was another hackathon for me, and I had fun taking part and working with my teammates. We didn't win. It did run from start to finish though, and I recorded a video of it for the hackathon and put it on LinkedIn.

An AI assistant is good at reading code and suggesting a fix. What it can't do on its own is touch your computer: run your tests, change a file, save the result. HealControl is the part that lets it. It offers the assistant twelve actions, things like "run the tests", "read this file", "write this file" and "open a pull request", and the assistant decides which to use and in what order. If you want a comparison, the assistant is the mechanic and HealControl is the toolbox. The toolbox decides what the mechanic can reach.

I wrote the server and the instructions the assistant follows, which tell it to act like an on-call engineer: find the failure, fix it, check the fix, no chatter. Teammates set up the demo scenarios.

The demo is one tiny program with a deliberate bug. A function is supposed to take 20 percent off a price of 200 and give 160. Instead it subtracts 20 and gives 180. Three small tests check this, and all three fail. The assistant runs them, sees the failures, reads the file, asks an IBM language model called Granite for a second opinion, rewrites the line and runs the tests again. If they pass, it saves the change on a separate copy of the code and opens a pull request on GitHub, which is the proposal a human then reviews.

The assistantdecides what to do HealControl12 actions to press Your codetests, version control, GitHub
Diagram. The assistant only reaches your code through these actions. It never touches the files itself.
The bug: 200 with 20% offprice - 20 = 180 The fix: take 20% of the priceprice - (price x 20 / 100) = 160 The tests expect 160.All three fail until that one line changes.
Diagram. The whole demo is this one line of arithmetic.

Not everything about it held up. The first version used IBM's own software library to talk to Granite, which was slow to start, so I replaced it with plain web requests. For a while the commands that save and push the change ran in the wrong folder, and I only fixed that on the last evening. I called it local-first, and that is only half true: when the assistant asks Granite for a diagnosis, the test output goes to IBM. And the rules are mostly just words. The assistant is told never to edit the tests, but nothing in the toolbox stops it. The one workflow rule the code enforces is that it won't push a change while the tests are failing.

The demo ran end to end on that toy program, and that is the only thing I have run it on. I have no success rate to show you, just the one recording. It also acts without asking first, which is fine for a toy bug and not for anyone's real code.

What's next. It did its job as a hackathon demo. Before it goes near real code it needs a step that asks before writing anything, and a lock on the test files.

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. We didn't win. The loop did run end to end on the demo app, and I recorded a video of it for the hackathon and posted it on LinkedIn.

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.

MCP clientBob, or any MCP client stdio src/server.py FastMCP, 12 tools healcontrol.json picks the active app _safe_path blocks paths outside the app Active app + pytestbroken_app/ (demo) git + gh CLIbranch, commit, push GitHubpull request IBM watsonx.aigranite-4-h-small, REST
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.
KindTools
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 Detectcheck_pipeline_status 2 Investigatelist_filesread_code_file 3 Diagnoseanalyze_with_watsonxREST call to IBM 4 Fixapply_surgical_fix 5 Verifyverify_fix 6 Shipcreate_git_branch, commit_fix,push_to_cloud tests still fail: back to step 4

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.

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).
TestExpectedBuggy returnsFixed returns
200 at 20%160180160
50 at 10%454045
75 at 100%0-250

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 didn't work

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 isn't true when analyze_with_watsonx is used: it sends the pytest output, tracebacks included, to IBM. And whatever model runs the client sees every tool result anyway.

Your machine MCP clientstarts the server server.py12 tools, stdio pytest, git, ghsubprocesses IBM watsonx.aipytest output, tracebacks too GitHubpushed branch, pull request The client's model providereverything the tools return Orange: the call that sends test output to IBM, whenever a diagnosis is asked for.
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 never got wired in: the mock failure data sits unused on the main branch, so it was a plan, not a feature.

What is unfinished

It only targets the demo app. The multi-app config exists, but only the demo app is in it, and I never wrote tests for the server itself. The only record of a full run is the video from the hackathon. I have no success rate and no log of what Granite replied. The agent also acts without asking for approval, which is fine for a toy bug and not for real code. What it needs first is 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 ran end to end; it did not win.
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 (a teammate's repo; the latest version is on the dev branch)

← All projects