Sunday, October 4, 2026

What Is an AI Agent? The Morning My Assistant Stopped Answering and Started Working

What Is an AI Agent? The Morning My Assistant Stopped Answering and Started Working

Last Tuesday morning, I had one of those starts every developer knows. Coffee half-made. Three meeting documents open. A slide deck half-finished. And my standup in twenty minutes.

I needed one short brief: what did we decide last week, and what is still open?

The old way I'd open a chatbot, paste in chunks of text, copy the answers out, and stitch everything together myself. Twenty minutes of copy-paste before I'd even started my day. It helped, but I was still doing all the work. The AI just answered.

So that morning I tried something different. I gave the AI a goal instead of a question:

"Go through these three documents. Pull out every decision we made and every question still open. Give me one short brief."

And then I watched it work.

It opened the first document and skimmed it. Then the second. Then the third. It pulled dates, names, decisions. Halfway through, it went back to page two of the first doc — it had missed a decision and caught itself.

Three minutes later I had my brief.

It made one mistake , it merged two meetings from different days into one. I pointed it out. It fixed it in ten seconds.

That small morning is when it clicked for me. A chatbot is like a friend who answers questions from memory. Good friend. Quick answers. But you still do everything else yourself. An agent is like an intern you hand a folder to. The intern reads the folder, makes a plan, does the work step by step, checks the result, and comes back with something finished.

Same AI brain. Completely different job.

Here's the simple way I remember it now:

Chatbot = talks. Agent = works.

You give an agent a goal. It breaks the goal into small steps. It uses tools — search, files, code, calendars. And it checks its own work as it goes. That's why agents are suddenly everywhere in 2026, quietly doing research, drafting reports, sorting support tickets, and writing first drafts of code while we do the parts that actually need a human brain.

One honest warning from my own Tuesdays: agents move fast, and fast work can be confidently wrong ,like my two meetings that became one. So for anything that touches money, messages to real people, or a final "yes," keep a human at the end of the line. Speed is a gift. Blind trust is not.

Start small. Hand it one clear task. Watch how it works. Then trust it with a little more.

That's exactly what I did that Tuesday — and my standup was ready before my coffee was.

Want to learn more?

Sunday, July 12, 2026

The two-minute VS Code fix that ended my "which window is which" problem

 I didn't realize how much a tiny thing was bugging me until I fixed it.

On any given day I've got four or five VS Code windows open. A client repo, some infra scripts, a notes folder, a sandbox I'm poking at. And every time I'd hover over the taskbar to switch between them, Windows would show me the same unhelpful thing for each one:
settings.json - my-project
package.json - my-project
README.md - my-project
The file name first. The project name second, usually cut off. So the one piece of information I actually needed — which project is this? — was the part getting truncated. I'd end up clicking through windows one by one like I was checking pockets for my keys.
Turns out VS Code lets you fix this in about two minutes.

The setting

There's a setting called window.title that controls exactly what shows up in the title bar (and, importantly, in that taskbar hover tooltip). By default it leads with the active file. You just flip it around so it leads with the folder name.
Open your settings JSON (Ctrl+Shift+P → "Preferences: Open User Settings (JSON)") and add:
"window.title": "${rootName}${separator}${appName}"
That's it. Now every window announces itself as:
my-project — Visual Studio Code
No file name, no clutter, just the thing I care about. Hover over the taskbar and I know instantly which project I'm looking at.

The variables, if you want to tweak it

VS Code gives you a handful of placeholders you can mix and match:
  • ${rootName} — your workspace or folder name (the project)
  • ${activeEditorShort} — the current file name
  • ${appName} — "Visual Studio Code"
  • ${separator} — a tidy — divider that politely disappears when a segment is empty
So if you do still want the file name, just put the project first where it belongs:
"window.title": "${rootName}${separator}${activeEditorShort}"
And if you like things to really jump out, wrap the project name in brackets:
"window.title": "[${rootName}]${separator}${activeEditorShort}"
Put this in your User settings (not a workspace .vscode/settings.json) so it applies to every project you open, not just one.

Bonus: give each project a color

Once I'd fixed the name, I went one step further. You can tint the title bar a different color per project, so you're not even reading — you just see which one you're in. Drop this in a repo's .vscode/settings.json:
{
  "workbench.colorCustomizations": {
    "titleBar.activeBackground": "#283324",
    "titleBar.inactiveBackground": "#394635"
  }
}
Now my client work is green, my infra repo is a deep blue, my sandbox is a slightly alarming orange. The name tells me the details; the color tells me at a glance. Muscle memory took over within a day.

Why such a small thing matters

None of this is clever. It's not a productivity hack that's going to 10x anything. But context-switching has a cost, and every little "wait, which window is this" moment is a tiny tax on your attention. Removing a dozen of those a day adds up to a workspace that just feels calmer.
Two minutes, two settings. Worth it.

Friday, May 1, 2026

Building a Python Async Client for an MCP Server

 MCP (Model Context Protocol) is gaining traction as a standard way for AI tools to call external services. Most examples show MCP clients in TypeScript. Here's how I built one in Python, wiring an npm-based MCP server into a FastAPI async backend.

The Idea

The MCP server I needed wraps the Jira API -- it exposes tools like jira_search_issues, jira_create_issue, and jira_get_transitions. The server speaks JSON-RPC 2.0 over stdio: you spawn it as a subprocess, write requests to its stdin, and read responses from its stdout.

Spawning the Server

Python's asyncio.create_subprocess_exec handles this cleanly:

self._proc = await asyncio.create_subprocess_exec(
    "npx", "--yes", "@xuandev/atlassian-mcp",
    stdin=asyncio.subprocess.PIPE,
    stdout=asyncio.subprocess.PIPE,
    stderr=asyncio.subprocess.PIPE,
    env=env,
)

Credentials (API token, domain, email) go into the env dict -- no hardcoding, no extra config files.

The MCP Handshake

Before calling any tools, MCP requires an initialization exchange. Send an initialize request, get capabilities back, then send a notifications/initialized one-way notification. Only after this handshake are tool calls available.

Request/Response Correlation

The tricky part with stdio JSON-RPC is that notifications can arrive interleaved with responses. The solution: attach an incrementing id to every request and skip responses until the matching id comes back.

async def _rpc(self, method, params, timeout=30.0):
    msg_id = self._next_id
    self._next_id += 1
    await self._send({"jsonrpc": "2.0", "id": msg_id, "method": method, "params": params})
    while True:
        resp = await self._recv(timeout=timeout)
        if resp.get("id") != msg_id:
            continue
        if "error" in resp:
            raise JiraMcpError(f"MCP error: {resp['error']}")
        return resp.get("result")

Each _recv awaits proc.stdout.readline(), so the loop yields to the event loop between reads -- no busy-waiting.

Lifecycle as a Context Manager

Wrapping everything in an async context manager keeps usage clean:

async with JiraMcpClient() as client:
    issues = await client.search_issues("project = MYPROJ ORDER BY priority ASC")
    new_key = await client.create_issue("MYPROJ", "Fix login bug", "Steps to reproduce...")

The subprocess spawns on __aenter__ and gets killed on __aexit__, exception or not.

What I Learned

The stdio transport is underrated. No ports, no HTTP server, no authentication layer -- just a process. If you need to integrate an MCP-compatible server into a backend that already uses asyncio, this pattern works well and is surprisingly easy to test: mock the subprocess with AsyncMock and feed it canned JSON-RPC responses.

The full client -- handshake, tool dispatch, response parsing, feature-flag gating -- came in under 200 lines of Python. That's a reasonable price for full Jira integration without touching the REST API directly.