Skip to main content
Glama
README.md
# asistonto-mcp

An MCP server that teaches any AI assistant to build **question packs** for
[asistonto](https://asistonto.escorpia.com), a live meeting copilot.

asistonto listens to a meeting and, the moment a question it was told to watch
for is actually being discussed, it surfaces that question. What it watches is a
**pack**. Writing a good one used to be expert work: the questions have to be
stated in a particular four-part format, and each of those parts belongs in a
different field because of what was measured about retrieval — get that wrong
and the pack loads fine and then stays dark all meeting.

This server hands that expertise to Claude, ChatGPT, Claude Code, Codex, or
anything else that speaks MCP. You paste your meeting notes; the assistant
writes the pack; the pack uploads to asistonto unchanged.

---

## Install

There is nothing to install by hand. Every client below runs the server through
`uvx`, which fetches and launches it on demand. You need
[uv](https://docs.astral.sh/uv/) and Python 3.12+.

The package is **not on PyPI yet**, so `uvx asistonto-mcp` will not find it.
Install it from the repository instead — this is the working command, and it is
the one used in every block below:

```sh
uvx --from git+https://github.com/R0DR0X/asistonto-mcp asistonto-mcp
```

That runs the server on stdio and waits, which is what an MCP client wants and
what looks like a hang in a terminal. Ctrl-C to leave. When PyPI publishing
happens the plain `uvx asistonto-mcp` will start working and every block here
gets shorter by three words; until then, use the `--from` form.

### Claude Code

```sh
claude mcp add asistonto -- uvx --from git+https://github.com/R0DR0X/asistonto-mcp asistonto-mcp
```

Or, for every project on this machine rather than one:

```sh
claude mcp add --scope user asistonto -- uvx --from git+https://github.com/R0DR0X/asistonto-mcp asistonto-mcp
```

Check it with `claude mcp list`; the line should end in `✔ Connected`. To upload
packs you also need an API token — see [Getting a pack into
asistonto](#getting-a-pack-into-asistonto) — which you can hand to the server at
registration time:

```sh
claude mcp add asistonto --env ASISTONTO_API_TOKEN=asist_... -- uvx --from git+https://github.com/R0DR0X/asistonto-mcp asistonto-mcp
```

### Claude Desktop

`claude_desktop_config.json` — macOS:
`~/Library/Application Support/Claude/claude_desktop_config.json`, Windows:
`%APPDATA%\Claude\claude_desktop_config.json`.

```json
{
  "mcpServers": {
    "asistonto": {
      "command": "uvx",
      "args": [
        "--from",
        "git+https://github.com/R0DR0X/asistonto-mcp",
        "asistonto-mcp"
      ],
      "env": { "ASISTONTO_API_TOKEN": "asist_..." }
    }
  }
}
```

Restart Claude Desktop afterwards. A desktop app has no shell to export a
variable in, so the token goes in `env` — which makes that config file a file
holding a secret. Keep it as private as the token itself, and leave `env` out
entirely if you only want to author packs and not upload them.

### Cursor, Windsurf, and anything else taking the generic block

The same `mcpServers` object, in whichever file that client reads
(`.cursor/mcp.json`, `~/.codeium/windsurf/mcp_config.json`, …):

```json
{
  "mcpServers": {
    "asistonto": {
      "command": "uvx",
      "args": [
        "--from",
        "git+https://github.com/R0DR0X/asistonto-mcp",
        "asistonto-mcp"
      ],
      "env": {}
    }
  }
}
```

### Codex

Codex keeps its configuration in `~/.codex/config.toml` — TOML, not JSON:

```toml
[mcp_servers.asistonto]
command = "uvx"
args = ["--from", "git+https://github.com/R0DR0X/asistonto-mcp", "asistonto-mcp"]

[mcp_servers.asistonto.env]
ASISTONTO_API_TOKEN = "asist_..."
```

### From a checkout

```sh
git clone https://github.com/R0DR0X/asistonto-mcp.git
uvx --from ./asistonto-mcp asistonto-mcp
```

---

## What it gives the assistant

**Tools**

| tool | what it does |
|---|---|
| `pack_authoring_guide` | the guidance below, for clients that cannot read resources |
| `compose_question` | assembles one question from the four parts, each into the right field |
| `parse_meeting_notes` | reads an existing corpus document, in either format asistonto uses |
| `check_paraphrases` | lints phrasings: too few, keywords, document vocabulary, sibling collisions |
| `validate_pack` | everything that would fail to load, plus everything that would fail *silently* |
| `write_pack` | writes `brief.json`, `paraphrases.json`, `background.txt`, `labels.json` |
| `read_pack` | loads an existing pack the way the live server does, and validates it |
| `upload_pack` | sends a pack to asistonto over HTTP, so a session can pick it |
| `render_question_document` | writes the questions back out as markdown a person can correct |
| `pack_size_floor` | how many questions a pack needs before anything in it can fire |

**Prompts** — `build_pack_for_meeting`, `write_paraphrases`,
`formulate_question`, `review_pack`.

**Resources** — `asistonto://guide/pack-format`,
`asistonto://guide/four-part-questions`, `asistonto://guide/paraphrases`,
`asistonto://guide/calibration`, `asistonto://guide/workflow`,
`asistonto://guide/publishing`, `asistonto://guide/labels`,
`asistonto://example/pack`, `asistonto://schema/brief.json`.

The resources matter as much as the tools. An assistant that reads the paraphrase
guidance before writing phrasings produces a materially better pack than one that
only calls functions — the quality lives in judgement, and the judgement is
written down there.

---

## Worked example, end to end

**1. Ask.** In any client with the server registered:

> Build me an asistonto pack for Tuesday's meeting with the warehouse lead about
> the new label. Here are my notes: *(paste)*. The spec says the label carries
> the lot code, but the picking screen we saw last week only shows the pallet id,
> and nobody has said which one the scanner reads.

**2. The assistant states each question in four parts** and calls
`compose_question`:

| part | content |
|---|---|
| 1 — where it came from | `ETIQUETAS_v3.md` line 41: "la etiqueta lleva el código de lote" |
| 2 — what was observed | the picking screen shown on 12 May displays only the pallet id |
| 3 — the question | when the operator scans at picking, does the code identify the pallet or the lot? |
| 4 — why it matters | the traceability chain hangs off which one is scanned |

which becomes one entry of `brief.json`:

```json
{
  "id": "QC-7",
  "title": "what the scanner reads at picking",
  "priority": "alta",
  "source": "ETIQUETAS_v3.md",
  "line": 41,
  "probes": ["When the operator scans at picking, does the code identify the pallet or the lot?"],
  "people": ["warehouse lead"],
  "rationale": "the traceability chain hangs off which of the two is scanned",
  "blocking": true,
  "context": "ETIQUETAS_v3.md line 41 says the label carries the lot code. The picking screen shown on 12 May displays only the pallet id."
}
```

The provenance sits in `context`, which is **never indexed** — indexing it costs
15 points of recall@1. The question sits in `probes`, the label in `title`, and
both of those are indexed.

**3. Ten paraphrases per question**, written from the question alone:

```json
{
  "QC-7": [
    "when they scan at picking, what are they actually reading",
    "the guy with the gun scans the pallet or the box",
    "does the scan tell you which lot it is or just where it is",
    "if two lots sit on the same pallet, how does the scan tell them apart",
    "what comes up on the screen after the beep",
    "can you tell from the scan which batch went out",
    "is the code on the label the same one the system stores",
    "do they scan once per pallet or once per unit",
    "what happens when the label is torn and it does not scan",
    "who decided which code goes on that label"
  ]
}
```

**4. `validate_pack`, then `write_pack`.** You get a directory:

```
packs/almacen/
├── brief.json         ← the only file the live loop reads
├── paraphrases.json   ← the editable source, folded into the brief
└── background.txt     ← a PAST meeting, for calibration
```

`write_pack` refuses two things outright: a pack that would not load, and a pack
too small for anything in it to ever fire. The second refusal is the important
one, because that pack loads perfectly and then does nothing (see [the three
things worth knowing](#the-three-things-worth-knowing-before-you-start)).

**5. `upload_pack`,** which puts it where the copilot can see it. That is the
next section.

---

## Getting a pack into asistonto

A pack on your disk is not a pack asistonto can use. The copilot runs on a
server; the pack has to be sent there over HTTP. That is what `upload_pack`
does.

### Once, per person

1. **Get an account.** Sign in at <https://asistonto.escorpia.com>. Sign-in goes
   through Google, so you need a Google account and you need to be able to reach
   the panel before any of this works.
2. **Create an API token** in the panel. It looks like `asist_…` and it is shown
   **once** — copy it then, not later.
3. **Put it in the environment the MCP server runs in**, as
   `ASISTONTO_API_TOKEN`. From a shell:

   ```sh
   export ASISTONTO_API_TOKEN=asist_...
   ```

   For a GUI client, use the `env` block shown in its section above.

### Running your own asistonto?

Set `ASISTONTO_BASE_URL` to your instance and everything below points at it
instead. That is necessary and **not sufficient**: `POST /packs/<name>` is only
registered when your instance has sign-in configured, and sign-in there is
all-or-nothing. Your server needs all four of

```sh
ASISTONTO_PUBLIC_ORIGIN=https://asistonto.example.com
ASISTONTO_OAUTH_CLIENT_ID=...
ASISTONTO_OAUTH_CLIENT_SECRET=...
ASISTONTO_OAUTH_DISCOVERY_URL=https://accounts.google.com/.well-known/openid-configuration
```

plus `ASISTONTO_ALLOWED_ACCOUNTS` listing the e-mail addresses allowed to sign
in — with any one of the four missing, the whole account system is off, there is
no panel to make a token in, and the upload route does not exist. Uploading to
such an instance answers `upload_not_available` (a bare `404` from the server).
Your instance also decides how large a body it accepts; the default is 4 MiB.

The server reads the token from the environment and from nowhere else. It is
never a tool argument — a tool argument is written into the transcript — and the
server never writes it to a file. Do not paste it into a chat.

### Per pack

Ask the assistant to upload it, or let it call the tool directly:

```
upload_pack(directory="packs/almacen")
```

It sends `brief.json`, `paraphrases.json` and `background.txt` to
`POST /packs/<name>`. The account that uploads a pack owns it.

### Then start the meeting

Open <https://asistonto.escorpia.com>, start a session, and pick the pack by
name. Pack names must be letters, digits, dot, dash or underscore, starting with
a letter or digit — the server rejects anything else.

### When it does not work

| what comes back | what happened | what to do |
|---|---|---|
| `token_missing` | `$ASISTONTO_API_TOKEN` is not set where the server runs | export it, or put it in the client's `env` block, and restart the client |
| `unauthorized` | the token is wrong, expired or revoked | create a new one in the panel and set it again |
| `name_taken` | the name is taken on that instance; the message repeats the server's own reason — another account owns it, or an unclaimed pack directory already has that name | choose another name: `upload_pack(directory=…, pack="almacen-turno-b")` |
| `rejected` | the pack is malformed; the message names the question | fix that question, `validate_pack`, upload again |
| `pack_cannot_fire` | the pack is under the size floor and would stay dark | add questions until there are at least 25 |
| `pack_too_large` | the body is over the instance's cap (4 MiB by default) | shorten `background.txt`, which is almost always what makes a pack big, or upload without it |
| `connection_reset` | the instance hung up while the body was being sent — almost always the same size cap, answered too late to arrive | same as `pack_too_large`; if the pack is small, retry and then check `ASISTONTO_BASE_URL` |
| `upload_not_available` | that instance has no upload route: a self-hosted asistonto without the four OAuth variables set | see [Running your own asistonto?](#running-your-own-asistonto) |
| `unreachable` | no network, or the wrong `ASISTONTO_BASE_URL` | check both |

---

## The three things worth knowing before you start

**Provenance is never a paraphrase.** Parts 1 and 2 describe documents;
paraphrases describe speech. Measured with paraphrases already indexed, adding
the provenance took recall@1 from 71.1% down to 55.6%. Worse, writing paraphrases
*while reading* the provenance pulls document vocabulary — codes, annex numbers,
file names — into phrasings that nobody ever says.

**Paraphrases must separate siblings, not reword them.** Six ways of saying the
same thing all resemble the neighbouring question equally, and 70% of measured
top-1 errors landed on a sibling of the same block. `check_paraphrases` flags
phrasings that fit the neighbour better than their own question.

**A small pack cannot fire at all.** The live gate is a z-score over the pack's
own scores, so its ceiling is `(n-1)/√n`. Against the default gate of 4.75 that
means **at least 25 questions** — below it the panel stays dark all meeting and
looks exactly like a quiet room. This is why `write_pack` and `upload_pack`
refuse a pack that small instead of warning about it.

The measurements behind all of this are in the guide resources, which the
assistant can read directly.

---

## Development

```sh
git clone https://github.com/R0DR0X/asistonto-mcp.git
cd asistonto-mcp
uv venv && uv pip install -e '.[dev]'
uv run pytest
```

The test suite runs the MCP server over a real client session and asserts that
what it writes round-trips through a copy of the product's own loader
(`tests/test_product_compatibility.py`). It never opens a socket: the upload
tests replace `upload.post_json`, the one function in the package that would.

## License

MIT.

TDQS

A3.6/5.0

Scored across 9 tools

Disambiguation4/5

Most tools have clearly distinct purposes: parsing notes, composing questions, checking paraphrases, validating packs, rendering documents, and reading/writing packs all target different stages. The only minor overlap is between validate_pack and read_pack, since read_pack also validates, but the descriptions make the distinction clear.

Naming Consistency3/5

The majority of tools follow a verb_noun pattern (compose_question, parse_meeting_notes, validate_pack, write_pack, read_pack), but two exceptions break the convention: pack_authoring_guide and pack_size_floor are noun phrases with no verb. This mixed pattern is readable but not fully consistent.

Tool Count5/5

With 9 tools, the server is well-scoped for its purpose. Each tool covers a distinct step in the pack authoring workflow, and there is no obvious redundancy or bloat.

Completeness4/5

The tool set covers the full authoring lifecycle: guidance, parsing, composing, paraphrasing, validation, rendering, writing, reading, and sizing. Minor gaps exist (no explicit delete or update operation, no batch listing), but write_pack can overwrite packs and the core workflow is complete.

Maintenance

ActivityMaintained
ResponsivenessNo issues