Learn AI Engineering
Lessons
← Loop Engineering

Connectors: Giving the Loop a Real Action via MCP

A connector turns the loop's last sentence into its last action, and because its tools have names, a hook can refuse the ones you can't take back.

Last lesson ended at a fixed branch and a report you have to act on yourself. What closes that gap is a connector. And whatever lets the loop act is also what lets you stop it acting, so this lesson has two halves: the action, and the gate.

What the report leaves you holding

Here is the kind of closing message last night's run leaves. It's a reconstruction, not a capture:

[03:14] ✓ pushed fix/apply-discount
        fixed test_apply_discount_larger_price
        suite: no new failures
        skipped: 1 test on the quarantine list (2.1)
        stopped on the prose condition — suite passes
next: open a PR from this branch

The fix is real, the suite is green, the branch is pushed. What it leaves for you, by hand, every morning:

  1. Open the pull request.
  2. Paste in which test was failing.
  3. Label it, so a reviewer knows a loop wrote it.
  4. Tell whoever has been waiting on it.

The clicking takes two minutes. The waiting takes six hours: the loop finished at 03:14 and the pull request opened at 09:14, because the last step was the one step that needed a person awake.

Trigger versus connector

Lesson 2.1 promised you this half. A trigger starts the loop. A connector is what the loop acts through once it's already running. They're easy to conflate, and this lesson is the connector half.

The standard is called MCP, the Model Context Protocol. You wire it once: one entry in a project file, .mcp.json, names the server. A plugin can also ship one of these for you, pre-wired. Once it's in, the loop gets new tools:

mcp__github__create_pull_request
mcp__github__add_issue_comment
mcp__github__merge_pull_request

These aren't shell strings. They're tools with names, listed and callable the same way Bash and Edit are. Read the names: server, then tool, joined by double underscores. Hold onto that shape, because it comes back.

Which actions it may fire unattended

This is the part that decides whether any of it is safe. Every action lands in one of two columns.

The loop fires these itselfThese wait for a person
Push the branch. It has done this since Lesson 2.1.Merge to master. A revert is a new commit, not an undo, and CI, deploys and everyone's next pull have already seen it.
Open the pull request, with the failing test named in the body.Deploy. The undo for a deploy is another deploy, and it's never instant.
Comment on the ticket that a fix is up for review.Anything sent outside your team: a client email, a status post. There is no unsend.

Everything on the left undoes in a single command (delete the branch, close the pull request, delete the comment), and nobody's phone buzzed.

So ask each action one question:

Can I undo this in one command, before anyone has seen it? Yes: the loop fires it. No: it waits for a person.

That question sorts an action you have never met before, which is more than a list of three can do.

Reversible is not the same as harmless, though. A pull request nobody asked for still costs somebody a review. Keep the left column short for a month before you grow it: every row you add is a thing that happens while you're asleep.

Gating the one you can't take back

You might be thinking: it already has a shell. gh pr merge is one Bash call away, so why wire a connector at all?

Because of the name. In a shell, a merge is a string inside a Bash call. Through the connector, it's a named tool, and a hook matcher is matched against the tool name. It's the same mechanism as Lesson 1.2.

Add a second entry to the same PreToolUse array in .claude/settings.json, beside the iteration cap, not instead of it:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash|PowerShell",
      "hooks": [{ "type": "command", "command": "python \"${CLAUDE_PROJECT_DIR}/.loop/guard.py\"", "timeout": 30 }]
    }, {
      "matcher": "mcp__github__merge_pull_request",
      "hooks": [{ "type": "command", "command": "python \"${CLAUDE_PROJECT_DIR}/.loop/gate.py\"", "timeout": 10 }]
    }]
  }
}

Then the gate itself, .loop/gate.py:

import json, sys
from pathlib import Path

QUEUE = Path(__file__).with_name("approvals.jsonl")

payload = json.load(sys.stdin)
tool    = payload.get("tool_name", "unknown")

# write the request down, so approving is reading a file
with QUEUE.open("a", encoding="utf-8") as f:
    f.write(json.dumps({"tool": tool, "input": payload.get("tool_input", {})}) + "\n")

print(f"{tool} is gated: a person approves this one.", file=sys.stderr)
raise SystemExit(2)  # 2 blocks the call; stderr goes back

It writes the request down, then exits 2, which blocks the call. There's no judgment in it, and no condition under which it allows the merge. The matcher is the policy; the script is just the paperwork. The gate isn't clever, and that's the feature.

So approval is not a prompt in the terminal. It's .loop/approvals.jsonl, you, and the merge button. The loop never gets a yes. It gets a no and a receipt.

The scripts are committed (Lesson 1.3), so every worktree already has gate.py.

Same command, a run that finishes the job

The command has not changed. Fourth lesson, word for word, nothing new in the prompt:

/loop 1d "Check the repo for newly failing tests. Rerun each 3x to rule out flakiness. If a real failure remains, fix it and keep going until the suite passes, then push the branch (not master)."

What the connector buys you:

  • The run ends in an action: a pull request with the failing test named in it, not a paragraph asking you to open one.
  • What it can't take back is a list: matchers you wrote, enumerable, diffable, the same in every session. One per tool, so you have to list them all.
  • The gate sits before the call, so it holds in every permission mode. That's Lesson 1.2's contract, unchanged.

What it doesn't:

  • It covers the connector, not the shell. gh pr merge inside a Bash call is a second door, and Lesson 1.2's Bash matcher is where you close it.
  • An open pull request is not a reviewed one. The connector removed the clicking, not the reading. That's Module 3, and it isn't optional.
  • It runs on your token. A connector is access the loop is holding, unattended, at three in the morning. The blast radius of a bad run just grew past your own working copy.

After this lesson

You will be able to:

  • tell a trigger, which starts a loop, from a connector, which the loop acts through;
  • read an MCP tool name as mcp__<server>__<tool> and know where it's wired;
  • sort any action by one question: can I undo it in one command before anyone has seen it?
  • gate an irreversible tool with a PreToolUse matcher and a script that always says no and leaves a receipt;
  • name the doors the gate doesn't cover, starting with the shell.

What comes next

A loop that can act unattended is a loop you need a way to switch off. In Lesson 2.5: stopping a loop, teardown, and the bounds /loop gives you for free.