Skip to content

Applied AI, with limits and review

I integrate models, agents, and MCP servers into systems already in production. Every integration has a written limit, and every generated line is reviewed before the deploy.

When I use a model

I use a model when the task repeats, has a clear input, and someone can review the output. Classifying free text, extracting data from documents, or preparing a first draft are good candidates.

If the task happens once a month, a script or a person handles it better and for less.

When I don’t

  • A slow query caused by a missing index. That gets measured and fixed in the database.
  • A badly designed data model. AI inherits the mess and repeats it faster.
  • A process nobody defined. Automating it only produces confusion with a better response time.

When the answer is code and not AI, I say so. More on that in when not to use AI.

Agents with scoped permissions

An agent gets the tools the task needs and nothing more. Each tool has a defined scope: what it can read, what it can write, and what gets logged.

Actions that change data or leave the system ask a person for confirmation. An agent without limits is an incident waiting its turn.

Diagram pending
What each of the agent’s tools can read, what it can write, and what gets logged.

MCP servers

MCP (Model Context Protocol) is an open protocol for connecting a model to in-house tools and data. I use it to expose only what the task needs, with separate credentials and no write access when the task only reads.

What the agent can’t read, it can’t leak.

Data that stays in

  • Credentials, keys, and secrets of any kind.
  • Customer personal data, unless there’s an explicit agreement.
  • Code or data the contract marks as confidential.

Before connecting an external provider’s model, I check what gets sent, what the provider keeps, and for how long.

How I audit generated code

I read what the model writes before it ships. Fast isn’t the same as correct.

  1. Input validation. What happens with an empty, huge, or malicious value.
  2. Contracts. Whether the function returns what the rest of the system expects.
  3. Error handling. Whether the error is logged, propagated, or swallowed.
  4. Dependencies. Whether a new library showed up, and who maintains it.
  5. Secrets. Whether a key, a token, or an internal URL ended up in the code.
  6. Tests. Whether a test fails when the change is wrong.

The full list, with examples, is in how I audit the code a model writes.

A review example

An illustrative example, written for this page. It doesn’t come from a client.

# What the model proposed
def find_sales(request):
    customer = request.args["customer"]
    sql = f"SELECT * FROM sales WHERE customer = '{customer}'"
    rows = db.execute(sql).fetchall()
    return {"rows": rows}

It compiles and returns data. The review turns up five problems:

  1. It builds the SQL from the value the user sends. That’s SQL injection.
  2. If the parameter is missing, the raw exception reaches the user.
  3. SELECT * returns columns the screen doesn’t use and maybe shouldn’t see.
  4. There’s no row limit. A customer with years of history pulls the whole table.
  5. No test covers any of these cases.
# What shipped after review
def find_sales(request):
    customer = request.args.get("customer", "").strip()
    if not customer or len(customer) > 64:
        return {"error": "invalid customer"}, 400

    sql = (
        "SELECT id, date, total FROM sales "
        "WHERE customer = %s ORDER BY date DESC LIMIT 100"
    )
    rows = db.execute(sql, (customer,)).fetchall()
    return {"rows": rows}

Cases and writing

The applied AI case isn’t published yet. I won’t fill it with a made-up example. In the meantime, the production system case studies show how I work on code that already exists.