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

MCP connector for a self-hosted [Shaarli](https://github.com/shaarli/Shaarli) bookmark manager. It speaks Shaarli REST API v1 over stdio and over stateless Streamable HTTP on localhost.

Shaarli is zlib software. This connector does not copy the PHP application. It is not affiliated with the Shaarli project.

`shaarli.routes.json` lists every `$this->get|post|put|delete` call in the `/api/v1` group of `index.php` (v0.16.10). All 11 routes are tool actions. None are excluded. `scripts/gen-operations.mjs` writes `src/operations.ts` from that file. Do not edit `src/operations.ts` by hand.

`SHAARLI_URL` and `SHAARLI_API_SECRET` are required. There is no default origin. Each request mints a JWT with header `{"typ":"JWT","alg":"HS512"}`, payload `{"iat": <unix seconds>}`, and HMAC-SHA512 of `header.payload` using the instance API secret. The client sends `Authorization: Bearer <jwt>`. Shaarli rejects `iat` in the future and `iat` older than 540 seconds (`ApiMiddleware::$TOKEN_DURATION`).

## 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": {
    "shaarli": {
      "command": "node",
      "args": ["/workspace/connectors/shaarli/dist/index.js"],
      "env": {
        "SHAARLI_URL": "http://127.0.0.1:8097",
        "SHAARLI_API_SECRET": "...",
        "SHAARLI_ALLOW_WRITES": "false"
      }
    }
  }
}
```

The API secret is the value from Shaarli administration, not a session cookie. The connector never prints it.

Docker, HTTP, no authentication, keep it on localhost.

```bash
docker build -t shaarli-mcp .
docker run -p 127.0.0.1:3000:3000 \
  -e SHAARLI_URL=http://127.0.0.1:8097 \
  -e SHAARLI_API_SECRET=... \
  shaarli-mcp
```

Do not put secrets in the image.

Regenerate the registry after a route bump.

```bash
npm run gen:operations
```

`scripts/live-check.mjs` talks to a real server over stdio. It reads the secret from `SHAARLI_API_SECRET` or from `SHAARLI_API_SECRET_FILE` (default `/workspace/tools/shaarli/api.secret`). It unsets `DATABASE_URL`. It prints pass or fail lines only, and it redacts the secret and JWTs. It does not run inside `npm run ci`.

## Config

| Variable                | Meaning                                                                                                                                                          |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SHAARLI_URL`           | Required. Origin only, `http` or `https`, no userinfo, query, hash, or path. Example `http://127.0.0.1:8097`. A URL whose path is already `/api/v1` is rejected. |
| `SHAARLI_API_SECRET`    | Required. HMAC key for per-request JWTs. Redacted in logs, errors, and tool results.                                                                             |
| `SHAARLI_ALLOW_WRITES`  | Must be the literal `true` before a confirmed write is sent.                                                                                                     |
| `SHAARLI_PER_MINUTE`    | Local request budget. Default 30. Cache hits do not spend it.                                                                                                    |
| `SHAARLI_TIMEOUT_MS`    | Upstream timeout. Default 20000.                                                                                                                                 |
| `SHAARLI_CACHE_TTL_SEC` | Read cache TTL. Default 60. Writes clear the cache.                                                                                                              |
| `SHAARLI_CONTACT`       | Contact token in the User-Agent. Must not contain the API secret.                                                                                                |
| `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/v1` plus the PHP path. `Accept` is `application/json`. JSON writes send `Content-Type: application/json`.

## Safety

Reads run without a write flag. A write still needs `confirm` set to the boolean `true` and `SHAARLI_ALLOW_WRITES=true`. The string `"true"` is rejected. Otherwise the tool returns a dry-run preview of method, path, query, and body, and it sends nothing. Omit `confirm` and `blocked` is false. Confirm true with writes disabled and `blocked` is true.

`PUT /api/v1/links/{id}` replaces title, url, description, tags, and private. The `update` action fetches the current link, merges the fields you pass, and PUTs the merged object. The preview body is that merged object.

HTTP 401 is unauthorized. HTTP 400 and 422 are invalid input. HTTP 404 is not found. HTTP 409 is conflict (duplicate URL). 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 secret and any JWT are redacted in logs, errors, and tool results.

## Tools

Three tools. Each takes `action` plus the fields for that action.

| Tool            | Actions                             | Routes                                                        |
| --------------- | ----------------------------------- | ------------------------------------------------------------- |
| `shaarli_links` | search, get, create, update, delete | GET/POST `/api/v1/links`, GET/PUT/DELETE `/api/v1/links/{id}` |
| `shaarli_tags`  | list, get, rename, delete           | GET `/api/v1/tags`, GET/PUT/DELETE `/api/v1/tags/{tagName}`   |
| `shaarli_info`  | info, history                       | GET `/api/v1/info`, GET `/api/v1/history`                     |

`shaarli_links` `search` is `GET /api/v1/links` with `searchterm`, `searchtags`, `visibility` (`all`, `public`, `private`), `offset`, and `limit` (a number, or `all`). Default limit on Shaarli is 20. `get` needs `id`.

`create` accepts `url`, `title`, `description`, `tags`, and `private`. An empty URL creates a note. A duplicate URL is HTTP 409.

`update` needs `id` and at least one of `url`, `title`, `description`, `tags`, `private`.

Shaarli tag path names match `[\w]+`. Hyphens and spaces do not match the route.

`history` accepts `since` (ISO 8601), `offset`, and a numeric `limit`. Shaarli rejects `limit=all` on history.

## Other MCP servers

`gh search repos "shaarli mcp"` returned no Shaarli MCP repository. npm search for `shaarli mcp` did not return a Shaarli connector. There is no published Shaarli MCP tool list to compare against. The official [python-shaarli-client](https://github.com/shaarli/python-shaarli-client) is a CLI, not MCP.

If you run [Linkding](https://github.com/sissbruecker/linkding) instead of Shaarli, use the sibling [linkding-mcp](https://github.com/si-essence/linkding-mcp) connector.

## Live check

See `LIVE-CHECK.md`. Run `unset DATABASE_URL` then `npm run live-check` against a Shaarli origin. The script never prints the API secret.

## License

MIT. Shaarli itself is zlib. This connector only talks to the published REST API.

TDQS

A4.1/5.0

Scored across 3 tools

Disambiguation5/5

The three tools are cleanly separated by resource: instance info/history, links, and tags. Each tool's description enumerates its actions, so an agent can easily pick the right tool and action without overlap.

Naming Consistency5/5

All tools follow a consistent shaarli_<resource> pattern in snake_case, with plural resource nouns where appropriate. The convention is predictable and easy to remember.

Tool Count5/5

Three tools is well-scoped for a Shaarli API wrapper, grouping operations by resource rather than exposing dozens of tiny endpoints. Each tool earns its place and covers a distinct domain area.

Completeness4/5

The surface covers the main lifecycle: info/history, full link CRUD plus search, and tag list/get/rename/delete. Minor gaps like explicit tag creation or additional link metadata operations are small and can be worked around.

Maintenance

ActivityMaintained
ResponsivenessNo recent community issues