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

> MCP server for the Lens suite.

**by svx** · MIT Licensed

[![CI](https://github.com/srivtx/lenses-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/srivtx/lenses-mcp/actions/workflows/ci.yml)
[![license](https://img.shields.io/badge/license-MIT-4f46e5)](LICENSE)

A [Model Context Protocol](https://modelcontextprotocol.io) server that exposes the five Lens-suite CLI tools to any MCP client (Claude Desktop, Cursor, etc.).

The Lens CLIs audit files **entirely offline** and return stable JSON. This server spawns the CLI, appends `--json`, captures stdout/stderr, and returns the parsed JSON as MCP text content. It never throws raw: non-zero exits come back as `isError: true` with the CLI's stderr and, when present, the parsed JSON.

## Tools

| Tool | Inputs | CLI |
| --- | --- | --- |
| `booklens_audit` | `path` (string), `fail_on?` (`error`\|`warning`\|`info`\|`none`) | `booklens audit <file> --json` |
| `booklens_fix` | `path` (string), `out?` (string), `dry_run?` (boolean) | `booklens fix <file> --json [-o <out>] [--dry-run]` |
| `officelens_audit` | `path`, `fail_on?` | `officelens <file> --json` |
| `odflens_audit` | `path`, `fail_on?` | `odflens <file> --json` |
| `iconlens_audit` | `path`, `fail_on?` | `iconlens <file> --json` |
| `waxseal_seal` | `archive`, `key`, `out?`, `created_at?` | `waxseal seal <archive> --key <pem> --json [--out <seal>] [--created-at <iso>]` |
| `waxseal_verify` | `archive`, `seal`, `public_key?`, `root?` | `waxseal verify <archive> -s <seal> --json [--public-key <pem>] [--root <hex>]` |
| `waxseal_inspect` | `archive` | `waxseal inspect <archive> --json` |

Exit codes from the CLIs: `0` ok, `1` findings, `2` bad input, `3` I/O. Any non-zero code is surfaced as an MCP error result.

> `iconlens` can read SVG from stdin in a terminal (`iconlens -`), but stdin is reserved by the MCP stdio transport, so over MCP always pass a file path.

## Install

Nothing to install — run it straight from GitHub with [Bun](https://bun.sh):

```sh
bunx github:srivtx/lenses-mcp#main
```

Or clone it and run from source:

```sh
git clone https://github.com/srivtx/lenses-mcp
cd lenses-mcp
bun install
```

## Use with an MCP client

### Claude Desktop

Add to `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "lenses": {
      "command": "bunx",
      "args": ["github:srivtx/lenses-mcp#main"]
    }
  }
}
```

### Cursor

Add to `~/.cursor/mcp.json` (or `.cursor/mcp.json` in a project):

```json
{
  "mcpServers": {
    "lenses": {
      "command": "bunx",
      "args": ["github:srivtx/lenses-mcp#main"]
    }
  }
}
```

To run from a local clone instead, use `"command": "bun"` with `"args": ["run", "/absolute/path/to/lenses-mcp/src/index.ts"]`.

### Any MCP client

Run `bunx github:srivtx/lenses-mcp#main` as a **stdio** server. It speaks newline-delimited JSON-RPC on stdin/stdout; all logs go to stderr.

## Command resolution and env overrides

By default each tool runs:

```
bunx github:srivtx/<tool>#main
```

The `#main` ref is **required** — a bare `github:` spec resolves whatever commit bunx has cached, which goes stale. The first call fetches the package over the network.

Set a `LENSES_*_BIN` variable to point at a binary you installed yourself (e.g. with the tool's `install.sh`) and skip the network entirely:

| Variable | Tool |
| --- | --- |
| `LENSES_BOOKLENS_BIN` | `booklens` |
| `LENSES_OFFICELENS_BIN` | `officelens` |
| `LENSES_ODFLENS_BIN` | `odflens` |
| `LENSES_ICONLENS_BIN` | `iconlens` |
| `LENSES_WAXSEAL_BIN` | `waxseal` |

When set, the override replaces the whole `bunx …` invocation:

```json
{
  "mcpServers": {
    "lenses": {
      "command": "lenses-mcp",
      "args": [],
      "env": {
        "LENSES_BOOKLENS_BIN": "/usr/local/bin/booklens",
        "LENSES_WAXSEAL_BIN": "/usr/local/bin/waxseal"
      }
    }
  }
}
```

An override that is an executable script (e.g. a local `.ts` CLI with a `#!/usr/bin/env bun` shebang) works too. A blank/whitespace value falls back to the `bunx` default.

## Development

```sh
bun test                      # unit tests for the command builders + validation
bun run typecheck             # tsc --noEmit
bun run start                 # run the stdio server
```

Smoke-test the MCP handshake and one tool call by piping newline-delimited JSON-RPC into the server. Set the override to a local checkout so the call does not hit GitHub:

```sh
LENSES_ICONLENS_BIN=../svg-a11y/src/cli.ts bun run src/index.ts <<'EOF'
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"smoke","version":"0.0.0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"iconlens_audit","arguments":{"path":"/absolute/path/to/icon.svg","fail_on":"none"}}}
EOF
```

The server prints one JSON-RPC response per request line (the `initialize` result reports `serverInfo.name: "lenses-mcp"`, and the `tools/call` result carries the CLI's JSON). Calling a missing file returns `"isError": true` with the CLI's stderr. All diagnostics go to stderr, never stdout.

## Layout

```
src/tools.ts     tool catalog, input validation, pure command builders
src/index.ts     stdio MCP server (Server + StdioServerTransport)
tests/           bun test unit tests
```