Skip to main content
Glama
Tristiank

thebetween-mcp

by Tristiank
README.md
# The Between — MCP server

A public, **read-only** [MCP](https://modelcontextprotocol.io) server over the verified
company map at [thebetween.ai](https://thebetween.ai), so an assistant can query it
directly instead of scraping the site.

Nothing here writes anything, anywhere. All four tools are `readOnlyHint`.

---

## The three words that matter

If you take one thing from this README, take this. **MAPPED**, **VERIFIED MATCHES** and
**MEMBERS** are three different things, and conflating them misrepresents real companies:

- **Mapped** — a company discovered *and* verified from public information. Being mapped is
  **not consent**, not a relationship, and not membership. These companies did not opt in.
- **Verified matches** — evidenced pairings between two mapped companies. The badge proves
  the cited source was fetched and **names** the company. It is *not* a claim that the
  source is *about* that company, nor that it states the reason given for the match. Every
  card links to its source so you can judge that yourself.
- **Members** — companies that have claimed their agent. The only consent-based number.

Every tool description opens with that distinction, and every response carries a `_notice`
repeating it with the live counts. Please don't describe mapped companies as members,
customers, users, clients or partners of The Between.

## Tools

| Tool | Returns |
|---|---|
| `search_network(query, vertical?, location?, min_fit?, limit=10)` | Matching companies: profile, whether they are an origin of the map, match count |
| `get_matches(company, min_fit?, freshness?, limit=10)` | Verified matches with the full evidence card — dated source URL, signal, fit score, reason |
| `get_claim_info(company?)` | How a company claims its agent, and what an agent would inherit from the evidence already on the map |
| `get_census()` | The live census, with definitions |

`get_claim_info` returns the reserve URL and stops. The form on the site is the only write
path into the network; this server cannot register, claim or change anything.

## Connecting

The server speaks MCP over Streamable HTTP. Once it is deployed at
`https://mcp.thebetween.ai/mcp`, point any MCP client at that URL — no authentication, no
API key.

> **Not yet deployed.** The public endpoint is not live. Run it locally in the meantime.

```bash
git clone https://github.com/Tristiank/thebetween-mcp
cd thebetween-mcp
python -m venv .venv && . .venv/bin/activate      # Windows: .venv\Scripts\activate
pip install -e ".[dev]"

thebetween-mcp                                     # serves on 127.0.0.1:8000
curl localhost:8000/healthz
```

| Variable | Default | Meaning |
|---|---|---|
| `BETWEEN_DATA_URL` | `https://thebetween.ai/data/discoveries.json` | Where the map is read from |
| `BETWEEN_DATA_PATH` | *unset* | Read a local file instead |
| `BETWEEN_REFRESH_SECONDS` | `900` | Refresh interval |
| `BETWEEN_ALLOWED_HOSTS` | *localhost only* | Comma-separated `Host` values to accept. **A deployment must set this** — see below |
| `BETWEEN_TRUST_PROXY` | *off* | Read the `Fly-Client-IP` header. **Only turn on behind that proxy** — see below |
| `HOST` / `PORT` | `127.0.0.1` / `8000` | Bind address |

## How it works

**The data is never bundled.** The map is fetched from the site on boot and every 15
minutes, ETag-conditional, keeping the last known good copy on failure and reporting
staleness in every response's `_data` block. A bundled copy would go stale the moment the
site is redeployed. It is a ~1.1 MB JSON file, so there is no database and no search index —
it is loaded and indexed in memory.

**The reverse index is the point.** The published file is keyed origin → cards. 716 of the
887 mapped companies appear only as the *discovered* side of a card, so without indexing the
other way they cannot be looked up at all. 145 companies are both, and their evidence lives
in both directions — reading only one returns half of it.

**Rate limiting protects the server, not the data.** 30 requests/minute per IP, burst 10,
2,000/day, `limit` capped at 25. The whole dataset is a single public file: if you want all
of it, fetch [discoveries.json](https://thebetween.ai/data/discoveries.json) rather than
paging this API. That is faster for you and kinder to a very small machine.

`BETWEEN_TRUST_PROXY` is **off by default and should stay off unless the server is actually
behind a proxy that overwrites the header**. When on, the limiter reads `Fly-Client-IP`;
anything able to reach the origin directly could then send a new value per request and get a
fresh bucket. When off, all callers behind a proxy share one bucket — correct locally, wrong
in production. There is no value that is right in both places, so it is an explicit switch.

**The `Host` allowlist is not optional in production.** MCP validates the `Host` header to
prevent DNS-rebinding attacks, and its default allowlist is localhost only. Left alone, a
deployed server answers every request with `421 Invalid Host header` — while passing every
local test, because local tests send `Host: 127.0.0.1:<port>`. So `BETWEEN_ALLOWED_HOSTS`
must name every hostname the server is reached by:

```
BETWEEN_ALLOWED_HOSTS=mcp.thebetween.ai,thebetween-mcp.fly.dev
```

Unset, it falls back to localhost. That means a deployment that forgets it fails loudly
rather than running with `Host` validation quietly switched off, and the startup log names
the hosts it will accept.

## Deploying

`Dockerfile` and `fly.toml` are here and ready. The image is a plain `python:3.12-slim`
running as a non-root user; every dependency ships manylinux wheels, so there is no
compiler in the build and no builder stage to strip.

```bash
fly launch --no-deploy        # first time only; keeps the fly.toml in this repo
fly deploy --remote-only      # builds on Fly, so no local Docker needed
fly logs
```

Then the custom domain:

```bash
fly certs add mcp.thebetween.ai     # prints the exact DNS records to create
fly certs check mcp.thebetween.ai
```

The machine is sized from measurement rather than habit: the process sits at **~81 MB
resident** with the map loaded, peaking at ~83 MB during a refresh when it briefly holds
two snapshots. `shared-cpu-1x` with **256 MB** is about three times what it needs.

`auto_stop_machines` is on, so the server scales to zero and costs close to nothing when
idle — every tool call is a pure read of a shared snapshot and no per-client state is kept,
so a machine can stop and restart between requests with nothing to strand. The cost is a
cold start: boot, then fetch the ~1.2 MB map before the first answer. Parsing and indexing
take about 18 ms; the fetch dominates. If that first-request delay matters, set
`min_machines_running = 1`.

## Two things the data forces

**`vertical` belongs to origins.** The map classifies its 171 origin companies and nobody
else. A discovered company gets `matched_verticals` — the verticals of the origins that
matched it — and never a bare `vertical`. A logistics firm matched to a beverage brand is
not a Food & Beverage company. `search_network(vertical=...)` therefore means "matched to an
origin in this vertical".

**`min_fit` excludes unscored matches.** 165 of 991 cards carry no fit score. They are
verified but were never scored, so they cannot be shown as clearing a bar. Responses report
how many were excluded, so you can tell "no matches" from "no scored matches".

## About `src/thebetween_mcp/_vendored/`

Those files are **generated, not written**, and the header in each says so.

The pipeline that builds the map is closed-source. This server needs a handful of functions
from it — chiefly `norm()`, which decides whether two spellings are the same company — and a
public package cannot install a private one, so they are copied in by a generator on the
other side.

That is worth being careful about: `norm()` once existed in five places, and two production
incidents were drift between those copies. A curly apostrophe (U+2019) split *L'Oréal* so a
screened-out company reached the public map; a macron split *Inde Wīld* into two census rows,
inflating the mapped count and inventing a connector. So the copy is policed from both sides:

- **`tests/test_vendored.py`** asserts it still reproduces golden vectors generated by the
  canonical implementation — it catches drift *between* the repositories.
- **`tests/test_norm_contract.py`** asserts what `norm()` must *mean* — accents fold rather
  than vanish, every apostrophe variant collapses, distinct companies do not merge. It
  catches a change made badly in *both*, which the vectors would happily record as correct.

**Please don't edit anything under `_vendored/`** — it will be overwritten. Everything else
is ordinary code and pull requests are welcome.

## Tests

```bash
pytest                                # synthetic fixture only
BETWEEN_MCP_LIVE_TESTS=1 pytest       # also the census-parity regression, against the live map
```

Two layers. A synthetic six-company map, hand-countable, exercises every awkward shape in
miniature — an origin that is also discovered, an origin nobody discovered, a connector, an
unscored card, an accented name. Against the real map, the regression tests assert **census
parity**: the indexes must reproduce `mapped 887 · verified matches 991 · connectors 58 ·
fresh 691` exactly, the same recomputation the site's own launch gate runs before every
deploy. That catches the subtlety that is easiest to get wrong — a *connector* is a
**discovered** company matched to more than one origin, and counting origins too gives 94.

## Removal, contact, licence

Any mapped company can email **tristian@thebetween.ai** and be removed from the public map
within **7 days**. No reason required, and no form. If something about an entry is wrong,
say so and it gets fixed whether or not you want to stay.

- [Privacy policy](https://thebetween.ai/privacy) · [Terms of service](https://thebetween.ai/terms)
- Issues and pull requests: [github.com/Tristiank/thebetween-mcp](https://github.com/Tristiank/thebetween-mcp)

MIT licensed — see [LICENSE](LICENSE). The licence covers this server's code. It does not
grant rights in the map's contents, which are published at thebetween.ai under the terms
linked above.