Skip to main content
Glama
kutt-mcp

kutt-mcp

by si-essence
README.md
# kutt-mcp

MCP connector for a self-hosted [Kutt](https://github.com/thedevs-network/kutt) v3.2.6 instance. It speaks the official JSON APIv2 over stdio and over stateless Streamable HTTP on localhost.

Kutt is MIT software. This connector does not copy the application. It is not affiliated with thedevs-network/kutt.

`spec/api-inventory.json` is a compact extraction of every OpenAPI path in `docs/api/api.js` from Kutt v3.2.6, prefixed with `/api/v2`. It records the release version, the official `package.json` sha256, `docs/api/api.js` sha256, `server/passport.js` sha256, and `routeTreeSha256`. `scripts/gen-operations.mjs` maps that inventory onto tool actions or exclusions and writes `src/operations.ts` and `kutt.routes.json`. A route that is neither mapped nor excluded fails generation. Do not edit `src/operations.ts` by hand. Default generation reads only the committed inventory.

`KUTT_URL` and `KUTT_API_KEY` are required. There is no default origin. Hosted `kutt.it` and `kutt.to` origins are rejected. Each request sends `X-API-KEY` matching `server/passport.js` `apiKeyHeader: "x-api-key"`.

## Setup

```bash
npm install
npm run ci
npm start
npm run start:http
```

`npm start` speaks stdio. `npm run start:http` serves Streamable HTTP at `http://127.0.0.1:3000/mcp` and `GET /health`. HTTP mode has no authentication of its own. Keep it on localhost.

```json
{
  "mcpServers": {
    "kutt": {
      "command": "node",
      "args": ["dist/index.js"],
      "env": {
        "KUTT_URL": "http://127.0.0.1:8101",
        "KUTT_API_KEY": "...",
        "KUTT_ALLOW_WRITES": "false"
      }
    }
  }
}
```

The key is the dashboard API key from the self-hosted instance. It is not a session cookie. The connector never prints it.

HTTP mode has no authentication of its own. Keep it on localhost. Do not put secrets in a Docker image.

```bash
docker build -t kutt-mcp .
docker run -p 127.0.0.1:3000:3000 \
  -e KUTT_URL=http://127.0.0.1:8101 \
  -e KUTT_API_KEY=... \
  kutt-mcp
```

Regenerate the registry from the committed inventory.

```bash
npm run gen:operations
node scripts/gen-operations.mjs --check
```

Refresh the inventory from an official v3.2.6 tree. This is the only command that reads route source.

```bash
node scripts/gen-operations.mjs --source-root /path/to/kutt --write-inventory
node scripts/gen-operations.mjs --source-root /path/to/kutt --check
```

`KUTT_SOURCE_ROOT` is accepted by a write run. `--check` ignores that variable unless you also pass `--source-root`.

`scripts/live-check.mjs` talks to a real server over stdio. Set `KUTT_URL`. Set `KUTT_API_KEY` or `KUTT_API_KEY_FILE` to a chmod 600 file outside this repo. The script unsets `DATABASE_URL`. It prints pass or fail lines only, and it redacts the key. It does not run inside `npm run ci`. A missing key is a failure.

## Config

| Variable             | Meaning                                                                                                       |
| -------------------- | ------------------------------------------------------------------------------------------------------------- |
| `KUTT_URL`           | Required. Origin only, `http` or `https`, no userinfo, query, hash, or path. Example `http://127.0.0.1:8101`. |
| `KUTT_API_KEY`       | Required. Sent as `X-API-KEY`. Redacted in logs, errors, and tool results.                                    |
| `KUTT_ALLOW_WRITES`  | Must be the literal `true` before a confirmed write is sent.                                                  |
| `KUTT_PER_MINUTE`    | Local request budget. Default 30. Cache hits do not spend it.                                                 |
| `KUTT_TIMEOUT_MS`    | Upstream timeout. Default 20000.                                                                              |
| `KUTT_CACHE_TTL_SEC` | Read cache TTL. Default 60. Writes clear the cache.                                                           |
| `KUTT_CONTACT`       | Contact token in the User-Agent. Must not contain the API key.                                                |
| `MCP_TRANSPORT`      | `stdio` or `http`. `--http` overrides.                                                                        |
| `PORT`               | HTTP port. Default 3000.                                                                                      |

Invalid values are reported by field name and issue code. The rejected value is not included.

Calls go to `/api/v2/...` paths from the operations registry. `Accept` is `application/json`. JSON writes send `Content-Type: application/json`. Redirects are refused so the key is not forwarded. There are no automatic retries. `kutt.it` and `kutt.to` are rejected at config load.

## Safety

Reads run without a write flag. A write is sent only when `confirm` is the boolean `true` and `KUTT_ALLOW_WRITES=true`. Omit `confirm`, or pass boolean `false`, and the tool returns a dry-run preview of method, path, query, headers, and body. It sends nothing. `blocked` is false on that preview, whether or not writes are enabled. `confirm: true` with writes disabled returns the same preview with `blocked` true and the reason `KUTT_ALLOW_WRITES is not true. The write was not sent.` The string `"true"` is rejected.

Omitted fields are dropped. `reuse: false`, `skip: 0`, and an empty `description` are sent when you set them. `null` is rejected. A field that is not valid for the selected action is rejected before any request is sent. Unknown fields are rejected.

HTTP 401 is unauthorized. HTTP 403 is forbidden. HTTP 400 and 422 are invalid input. HTTP 404 is not found. HTTP 409 is conflict. HTTP 429 includes Retry-After when the header is present. Other failures are server, network, or timeout. Response bodies are capped at 1,000,000 bytes.

The API key, password, and session fields are redacted in logs, errors, and tool results. Upstream JSON error bodies are parsed and those fields are replaced before the text is returned or logged. A password you set on that call is removed from the success result, the error text, the preview, and the log, including a password of 3 to 7 characters. Percent-encoded and JSON-escaped copies of that password are removed too. Other short words are left in place. The API key still has to be at least 8 characters. `GET /users` is projected to `email` and `domains`. `apikey` is never returned.

## Tools

Three tools. Each takes `action` plus the fields for that action. Fields for a different action are rejected.

| Tool           | Actions                             | Routes                                                                                      |
| -------------- | ----------------------------------- | ------------------------------------------------------------------------------------------- |
| `kutt_links`   | list, create, update, delete, stats | GET/POST `/api/v2/links`, PATCH/DELETE `/api/v2/links/{id}`, GET `/api/v2/links/{id}/stats` |
| `kutt_domains` | list, create, delete                | GET `/api/v2/users` (domains only), POST `/api/v2/domains`, DELETE `/api/v2/domains/{id}`   |
| `kutt_info`    | health, me                          | GET `/api/v2/health`, GET `/api/v2/users`                                                   |

`kutt_links` `list` is `GET /api/v2/links` with `limit`, `skip`, documented `all`, and source-supported `search`. v3.2.6 `GET /links` still scopes to the user when `all` is set. There is no native GET `/links/{id}`. Stats is the per-id read.

`create` needs `target`. Optional `description`, `expire_in`, `password`, `customurl`, `reuse`, and `domain` are sent when set, including `reuse: false` and empty `description`.

`update` needs `id` and at least one of `target`, `address`, `description`, `expire_in`, or `password`. Official `editLink` treats a falsy description as unchanged.

`kutt_domains` `list` is derived from `GET /users` after scrubbing `apikey`. Create needs `address`. Optional `homepage` is sent when set. Official `addDomain` does not DNS-check.

`kutt_info` `me` returns email and domains. Health returns `{ status }` from the plain text `OK` body.

9 of 9 official documented APIv2 operations are mapped. 0 are excluded. The registry has 9 actions, one for each HTTP operation. Callers see 10 actions. `kutt_domains` `list` is derived from `GET /users` and is not another HTTP route. Auth, admin HTML, ban, report, and user-deletion routes are outside the OpenAPI inventory. See `kutt.routes.json`.

## Other MCP servers

[reklis/kutt-mcp](https://github.com/reklis/kutt-mcp) (MIT, `dev` branch read 7 Nov 2026) registers nine tools in `server.py`: `health_check`, `list_links`, `create_link`, `update_link`, `delete_link`, `get_link_stats`, `create_domain`, `delete_domain`, `get_user_info`. Stats, domains, and user info already exist there. Those reads are not unique here.

`create_link` (lines 150-165) builds a payload and calls `client.post("/links", json=payload)` with no `confirm` argument and no write-environment guard. `update_link` (188-197), `delete_link` (210-213), `create_domain` (253-260), and `delete_domain` (273-276) call `patch`/`post`/`delete` the same way. A search of `server.py` found no `confirm` input and no `ALLOW_WRITES` check.

`get_user_info` (280-292) returns `GET /users` JSON. The docstring lists `apikey`. Optional `description` fields are added only `if description:`. `list_links` takes `limit`, `skip`, and `all_links` only. `KUTT_BASE_URL` defaults to `https://kutt.it/api/v2`.

This connector does not add local filesystem or external download tools. Every write is sent only when `confirm` is true and `KUTT_ALLOW_WRITES=true`. Omit `confirm` or pass false and the tool returns a preview and sends nothing. Hosted kutt.it/kutt.to origins are rejected. `me` never returns the API key. Empty strings and `reuse: false` are sent when the caller sets them. `list` can pass source-supported `search`. The route inventory is pinned to v3.2.6.

## Limitations

- There is no documented GET `/links/{id}`. This connector does not invent one. Use `list` or `stats`.
- Official `editLink` (`server/handlers/links.handler.js`) applies description only when it is truthy. Sending `description: ""` does not clear a stored description.
- Official `createLink` description uses `checkFalsy: true` and min length 1, so an empty create description is skipped by validators.
- Documented `all` on GET `/links` is unused by v3.2.6 `links.get`. The handler always scopes to `req.user.id`.
- Missing-link handlers often throw `CustomError` without a status, which is HTTP 500 in official code. Live missing-id rows record the actual status.
- Auth, admin tables, ban, report, signup, password reset, and API key rotation are outside the documented APIv2 inventory.
- Official `app.listen(PORT)` has no host argument, so a local process binds every interface on that port.
- The local Kutt process in `LIVE-CHECK.md` runs on Node 22. `better-sqlite3` 11.8.1 has no Node 24 prebuild here, and this environment has no `make`.

## Live check

See `LIVE-CHECK.md` for the checks that actually ran, including first-start failures. `npm run live-check` is not part of `npm run ci`.

`npm run ci` typechecks, lints, tests, and builds. `node scripts/gen-operations.mjs --check` reports 9 mapped routes, 0 excluded, 9 registry actions for the documented HTTP operations, 10 exposed actions including the derived domain list, and 9 routes in the spec.

Run `unset DATABASE_URL`, set `KUTT_URL`, and set `KUTT_API_KEY` or `KUTT_API_KEY_FILE`. Then run `npm run live-check`. The script never prints the API key.

## License

MIT. Kutt itself is MIT. This connector only talks to the published JSON API.

TDQS

A3.9/5.0

Scored across 3 tools

Disambiguation4/5

The three tools map to clearly separate resources: short links, custom domains, and server/user info, so there is little risk of choosing the wrong one. The only residual ambiguity is inside each tool, where the action parameter selects among list/create/update/delete/stats, so an agent must read the enum rather than the tool name alone.

Naming Consistency5/5

All three tools follow a uniform kutt_<resource> convention with plural noun resources (kutt_links, kutt_domains, kutt_info). There is no mixing of casing or verb styles, and the sub-actions are consistently expressed through an action parameter instead of proliferating verb-prefixed names.

Tool Count4/5

Three tools is on the thin side but is justified here because each aggregates a whole CRUD surface via an action dispatcher, keeping the surface compact rather than fragmenting into a dozen verb tools. It sits at the low end of the comfortable range, which is acceptable but leaves little granularity.

Completeness4/5

Links cover list/create/update/delete/stats and domains cover list/create/delete, plus health and authenticated profile reads, which matches the core Kutt workflow. The explicit absence of a single-link GET and the lack of domain update/verification or user-profile update are minor gaps an agent can work around.

Maintenance

ActivityMaintained
ResponsivenessNo recent community issues