Best Use of AI Yet!

· 7min

I set out to see if I could build the most impactful, technically impressive, and world changing use for running local AI models. Instead, I built a fun yet completely impractical MCP server to control Niri.

The Problem

"How can AI help my daily workflow?" This question is being asked and answered in innumerable ways by countless people. "What is a absurd project I can build with AI?" Is the question I chose to tackle.

I use a few different window managers based on my mood: pinnacle, niri, sway, and sometimes mango. One day I decided to build some tooling to control one of these environments using AI.

Is this helpful in some way? Absolutely not. Nothing says "how do I make a keyboard-centric environment better" like introducing AI (/s). But, alas, the curiosity had already taken hold.

The Plan

Having now decided to give LLM automation of Niri a try, I first needed to come up with a list of things to control via an agent. To do this, I asked niri for a list of available commands:

niri msg action

Which produces a list of commands and their description:

quit
      Exit niri
power-off-monitors
      Power off all monitors via DPMS
power-on-monitors
      Power on all monitors via DPMS
spawn
      Spawn a command
...

The next step was to translate these commands into a format an agent could call upon to do something.

Enter FastMCP.

Local LLM Setup

For the sake of completeness, I should lay out the setup I am using for all this.

  • System: 2025 Zephyrus G14: 32gb system RAM + 5070ti (12GB VRAM)
  • Ollama (locally hosted): with the context window set to 32000
  • Open WebUI: Running in podman locally
  • Model: I ran this a couple of times using different models:
    • Qwen3 Coder (Unsloth)
    • Devstral2 small
    • GPT OSS 20b

Creating an MCP

Model Context Protocol is a way to provide an AI system methods to fetch data or perform some action based on a set of descriptions and code. The agent or application doesn't actual know how the data is fetched or the action is called, it simply knows "if I talk to this other thing and provide it some defined parameters, I should get something helpful back."

So here is where I took our list of commands + descriptions and asked my local model to create a FastMCP implementation around the them. I attached a text file with the list of commands and descriptions in OpenWebUI and used Qwen3 Coder with the following initial prompt:

i need to create a fast mcp server based on a set of commands and descriptions. the commands will be executed as a subprocess in the form niri msg action <command>. i will attach a file with the commands on a line followed by the description on a new line. use the description to create the fastmcp descriptions/comments.

This almost worked as anticipated, the format wasn't quite right and the functions generated didn't actually execute the commands 🤦. I tried to fix it.

this was a good start, but the commands in the file should be translated to an mcp tool and should use python subprocess to call the niri msg action <command>

The functions were modified correctly to execute the subprocess correctly, but the fastmcp decorators were not correct.

this is almost right. please replace all decorators with @mcp.tool

Now we are cooking. I copied the output and placed it in a server.py file. In the same directory I installed fastmcp using:

uv init
uv add fastmcp

We can now run the MCP server with:

uv run fastmcp run server.py:mcp --transport http --port 8000 

The MCP server is now listening on http://localhost:8000/mcp.

Wiring It Up

The next step to actually using the MCP is to hook it up to something that can leverage tools and call them. Luckily for us OpenWebUI can do that:

  • Click on the profile icon in the top right of OpenWebUI
  • Select "Settings"
  • Click "Admin Settings"
  • Next find the "External Tools" section
  • Click "Add Connection" plus symbol on the right
  • Change the "Type" to "MCP Streamable HTTP"
  • Set the URL base to http://localhost:8000/mcp and click the test connection button
  • Set the authentication to None

Back in your chats, you will see an icon in the input box for "integration", select that, navigate through an sub-menus until you find your MCP and enable it. Once you have done this you can now issue commands via the chat window to control things in your Niri environment.

It's fine to pick a smaller fast model that can handle tool calling here, we don't really need anything terribly smart. I had Mistral 2 14b already available so I decided to just use that.

My system prompt is something along the lines of:

you are an agent whose sole purpose is to take user input and utilize the Niri MCP to accomplish tasks.

Then I simply can communicate via OpenWebUI:

toggle the overview

and 💥 things work as expected.

Extra Fun

You can also use the speech recognition and voice mode of OpenWebUI to control Niri through our new MCP by speaking commands instead of typing.

Notes

If you open up the server.py we generated at the beginning of this adventure, you may notice some of the Niri commands are missing. This will probably vary depending on the model you used or the exact back and forth via chat. In this case, I just manually added a few missing things.

Additionally, there are some calls that require some extra parameters and that's somewhat easy to add:


@mcp.tool
def move_column_to_workspace():
    """Move the focused column to a workspace by reference (index or name)"""
    subprocess.run(["niri", "msg", "action", "move-column-to-workspace"])
    return "Column moved to workspace"

Becomes:


@mcp.tool
def move_column_to_workspace(index: int):
    """Move the focused column to a workspace by reference (index or name)"""
    subprocess.run(["niri", "msg", "action", "move-column-to-workspace", str(index)])
    return "Column moved to workspace"

Wrapping Up

This was a interesting little experiment to build something I hadn't seen yet, using some new (to me) tooling. Is it useful? Definitely not. In any case, I'm a fan of continuous learning and this was enlightening on a number of fronts.

I think this exercise highlights a couple of interesting things. First, local models are not nearly as smart as the cloud versions and often require more precision and back and forth to get things right. This can be challenging since the exchange grows the context window which can lead to poorer outputs the longer the conversation is maintained. Because of this, it becomes important to enter into the project with some well constructed prompts up front to try and knock out the task in as limited a conversation as possible. Secondly, most of the local models have a better foundational understanding of some technologies than others. I was surprised that FastMCP creation wasn't slightly more trivial than it turned out to be, and my anecdotal experience trying to use Qwen3 Coder for rust development has been similarly hit-and-miss. However, the coding models are generally very well versed in more mainstream python and javascript frameworks. Despite the paper cuts, this experiment demonstrates that LLM/AI tools are evolving quickly.

My main take away is that this technology continues to improve at a very rapid rate. Attempting this same project with an actual coding assistant would undoubtedly yield better results faster, and I will likely reattempt this with OpenCode shortly. I would encourage every developer to at least maintain some contact with the frontier of the AI space, even if your previous experience left something to be desired. It's always good to keep learning, and I don't believe this tech is going away anytime soon.