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.