Skip to main content
  1. 🏃 Usage/

Serving to an Agent (`mcp` command)

·650 words

The mcp command serves the birthmark pipeline over the Model Context Protocol, on stdin and stdout. An agent can then ask whether two programs are alike without a person chaining extract and compare by hand.

The subcommand is behind a cargo feature. The released binaries and both container images carry it; building from source needs it asked for:

cargo build --release --features mcp

⚙️ Configuring a client
#

.mcp.json, in a project directory:

{
  "mcpServers": {
    "oinkie": {
      "command": "oinkie",
      "args": ["mcp", "--root", "."]
    }
  }
}

For a container, use the light image — the tools here never lift, so the Ghidra in the full image is a gigabyte of dead weight:

{
  "mcpServers": {
    "oinkie": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-v", "/path/to/your/work:/work",
        "quay.io/tama5/oinkie:light",
        "mcp", "--root", "/work"
      ]
    }
  }
}

-i keeps stdin open, which is the whole transport. Do not add -t: a TTY rewrites what passes through it and breaks the JSON-RPC framing.


🧰 The tools
#

toolwhat it is for
oinkie_infothe vocabulary: birthmark types, algorithms, analysis names, strategies, aggregators
oinkie_runlifted programs in, a similarity per pair out — the whole question in one call
oinkie_extractwrite birthmarks, which oinkie_compare takes
oinkie_comparescore birthmarks, to try another algorithm without re-reading the programs
oinkie_reaggregaterecompute scores in a directory under a different aggregator

An agent should ask oinkie_info first. The names are precise, and its lists are generated from the same code that parses them, so they cannot drift from what is accepted.

A destination directory is optional for oinkie_run and oinkie_compare, and is written in the shape run -d produces — which is what lets a directory one of them wrote be handed straight to oinkie_reaggregate.


📁 Confining what it touches
#

Every path the tools are given, input and output alike, has to resolve inside a --root. Repeat the option for more than one; it defaults to the working directory.

The paths reaching these tools are written by a language model rather than by you, and a model with the wrong directory in mind will say so with a path rather than a question. --root is what keeps that a refusal instead of a file written somewhere surprising.


🚫 Lifting is not offered
#

There is no oinkie_lift, and there is not going to be one. Run oinkie lift yourself and point the tools at what it produced — on a host with Ghidra installed, or in the full image, which bundles it.

It is the one step whose shape does not fit a tool call. It starts a whole decompiler process per binary, and how long that takes is set by the binary rather than by the request: a hello-world lifts in a few seconds, and something worth comparing takes considerably longer, with the client blocked on a single call for the whole of it. --script is the other half — a replacement lifting script is arbitrary code inside Ghidra, which is a reasonable thing to put in a person’s hands and not in a model’s.

None of that says lifting is dangerous. A model with a shell can run oinkie lift like anyone else, and it is welcome to. The line here is about which step belongs inside a request and response, not about what may be run.


🩺 When something is wrong
#

The server speaks JSON-RPC on stdout and nothing else; logs and errors go to stderr. If a client reports a protocol error, check whether something else in the environment is writing to stdout.

A refused name comes back in the library’s own words, naming the spelling that was meant:

op-seq-euclidean: euclidean operates on frequency vectors; use op-freq-euclidean.

A refused path names the directories that are allowed. A request for more than 500 pairs is refused before anything is read — all-and-self over \(n\) files is \(n(n+1)/2\) pairs — and max_pairs raises that deliberately.