Skip to main content
Glama
mariansan

lego-catalog

by mariansan

lego-catalog-mcp

A monorepo of read-only Model Context Protocol (MCP) servers, all served from one Vercel domain under /<server>/mcp. The first (and currently only) server is lego-catalog.

1. What it is

lego-catalog is a read-only remote MCP server (Streamable HTTP, stateless) that exposes the Rebrickable LEGO catalog (parts, colors, set inventories) from a SQLite snapshot. The goal: an agent can verify part IDs and colors instead of inventing them.

Deployments require a bearer token (MCP_AUTH_TOKEN): the data is public, the endpoint is not.

Development happens in a private source repository; the public copy at https://github.com/mariansan/lego-catalog-mcp is a reference copy updated from it periodically and runs no CI or deploy automation.

Monorepo layout:

Path

Role

mcps/<name>/

One self-contained MCP server (source, tests, snapshot build scripts)

api/<name>/mcp.ts

Thin Vercel function adapter for that server

/<name>/mcp

Public URL path (rewritten to the function in vercel.json)

scripts/

Snapshot sanity check and smoke test

docs/

Self-hosting guide and security review

Related MCP server: akm-mcp

2. Your endpoint and how to add it in Claude

This repository does not publish a hosted endpoint. Run the server locally or host your own copy (see the self-hosting guide). Your endpoint will look like:

https://<your-domain>/lego-catalog/mcp

In Claude: Customize -> Connectors -> Add custom connector -> paste your URL -> Authentication: No sign-in, then under Request headers add authorization = Bearer <your token> and mark it Required. Request headers are in beta and available to a limited set of organizations; Claude does not show a header value again after you save it, so changing the token means removing and re-adding the connector [source: Claude's custom connector documentation, read 2026-10-06].

Use the FULL path. The origin alone (https://<your-domain>) is not an MCP endpoint: it answers 405 to MCP requests, and Claude then shows a misleading sign-in warning. The full path answers 401 without the token.

The self-hosting guide covers adding the connector (including Team/Enterprise steps and per-chat enabling), running locally and hosting your own copy.

3. Tools

All six tools are read-only (annotated readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false). Arguments are validated strictly: unknown keys are rejected with invalid_input. Argument types, defaults and bounds below are copied from the zod schemas in mcps/lego-catalog/src/tools.ts and config.ts.

Tool

Purpose

search_parts

Full-text search over part names and part numbers

get_part

One part by part_num: category, material, colors, element records

list_colors

The Rebrickable color palette, filterable

get_set_inventory

Part list of a set (sets only, not minifigures)

snapshot_info

Snapshot date, source, row counts, limits

lookup_element

Resolve an element_id to part and color

ID vocabulary

  • part_num is Rebrickable's part number: usually, but not always, the LEGO design number (printed/assembled variants such as 6895a or 90462pr0004 have their own part_num).

  • element_id identifies a design in one color.

  • design_id exists only on element records (the elements table) and may be null.

  • No BrickLink, LDraw or other external ids are available. color_id is Rebrickable's color id.

search_parts

Every word in query must match; results are ranked by relevance. Use it to find the right part_num; never guess one.

Argument

Type

Default

Bounds

query

string (required)

-

trimmed, 1-200 chars, at most 20 distinct words; punctuation ignored

limit

integer

25

1-100

offset

integer

0

0-10,000

{"name": "search_parts", "arguments": {"query": "brick 2 x 4", "limit": 5}}

Verified on the live deployment (2026-10-05): this query returns 3001 "Brick 2 x 4" first, total 1708.

get_part

Returns name, category, material, up to 100 colors (most-used first) and up to 25 element records (element_id, color_id, design_id), plus colors_total, elements_total and *_truncated flags. Unknown ids return not_found (ids are case-sensitive).

Argument

Type

Default

Bounds

part_num

string (required)

-

trimmed, 1-64 chars, e.g. "3001"

{"name": "get_part", "arguments": {"part_num": "3001"}}

Verified on the live deployment (2026-10-05): category Bricks, 78 colors, 148 elements.

list_colors

About 275 colors (id, name, RGB hex, transparency, num_parts, num_sets, first_year, last_year), ordered by color_id.

Argument

Type

Default

Bounds

name

string (optional)

-

trimmed, 1-64 chars; case-insensitive substring

is_trans

boolean (optional)

-

true = transparent only, false = opaque only

limit

integer

100

1-100

offset

integer

0

0-10,000

{"name": "list_colors", "arguments": {"name": "blue", "is_trans": false}}

get_set_inventory

Part list of a set. "75192" is read as "75192-1". Minifigure ids (fig-...) are not supported and return not_found. Uses the highest inventory version unless version is given; the response lists available_versions. Rows are ordered by part_num, color_id; is_spare rows are not part of the built model; total_quantity counts non-spare pieces.

Argument

Type

Default

Bounds

set_num

string (required)

-

1-32 chars of letters, digits, ., _, -

version

integer (optional)

highest available

1-1000

limit

integer

25

1-200

offset

integer

0

0-10,000

{"name": "get_set_inventory", "arguments": {"set_num": "75192-1", "limit": 50}}

Verified on the live deployment (2026-10-05): "Millennium Falcon", 2017, inventory version 2 of [2, 1], 726 rows. An unknown set returns:

{"error": {"code": "not_found", "message": "No set \"99999999-1\".", "hint": "Set numbers look like \"75192-1\" (number-version)."}}

snapshot_info

No arguments. Returns snapshot date, source URL and attribution, build and download timestamps, schema version, the inventory-version rule, per-table row_counts, the server's limits, and the ID notes. Call it to tell the user how fresh the data is.

{"name": "snapshot_info", "arguments": {}}

Row counts at the 2026-10-05 snapshot: colors 275, parts 64,826, sets 28,444, inventories 47,671, inventory_parts 1,565,757, elements 114,601.

lookup_element

Resolves an element id (one design in one color, as on packaging / Pick a Brick) to part (part_num, name), color (color_id, name, rgb) and design_id (may be null and may differ from part_num). Unknown ids return not_found.

Argument

Type

Default

Bounds

element_id

string (required)

-

trimmed, 1-12 digits

{"name": "lookup_element", "arguments": {"element_id": "300126"}}

("300126" is the format example from the tool's schema; its lookup result was not checked for this README.)

4. Responses and limits

Every response from the tool handlers, success and error alike, carries source ("Rebrickable") and snapshot_date. Results are returned both as structuredContent and as a JSON text block.

Errors are tool results with isError: true and the shape {"source", "snapshot_date", "error": {"code", "message", "hint?"}}. Codes: not_found, invalid_input, unknown_tool, response_too_large, internal_error. Missing data is a structured not_found, never an empty guess.

Known exception: errors raised by the MCP SDK before a handler runs (for example tools/call with non-object arguments) come back as a JSON-RPC -32603 error without the envelope. A known, accepted limitation; see SEC-L2 in docs/SECURITY_REVIEW.md.

Pagination (search_parts, list_colors, get_set_inventory): requests take limit and offset; responses add total, offset, limit, returned, has_more, next_offset (null on the last page) and size_capped.

Limit

Value

Default page size

25 (list_colors: 100)

Max page size

100 (get_set_inventory: 200)

Max offset

10,000

Search query

200 characters, 20 distinct words

Response payload budget

20,000 characters; rows are dropped past it and size_capped: true is set. next_offset then points at the first omitted row, so paging loses nothing

Hard refusal

A payload still over 24,000 characters becomes response_too_large

Request body

64 KiB (larger gets HTTP 413)

JSON-RPC batches

Rejected (HTTP 400)

GET / DELETE

HTTP 405 (stateless: no stream or session to manage)

Snapshot missing or unreadable

HTTP 503 Service unavailable: catalog not loaded. (JSON-RPC error, no source/snapshot_date)

Because of the payload budget, get_set_inventory with limit=200 may return fewer rows than requested; follow next_offset.

Latency, for orientation only: a sequential client-side sample from one machine (2026-10-05, includes network) gave search_parts p50 about 130 ms and p95 about 200 ms. This is not a cold-start or load test and is not an SLA.

5. Data and refresh

  • The snapshot is a SQLite file built from Rebrickable's CSV downloads by pnpm --filter @mcps/lego-catalog build:snapshot.

  • The hosted instance's snapshot is rebuilt periodically by the maintainer. Every response carries snapshot_date.

  • Rebrickable's Downloads page allows automated downloads of the zipped CSVs at most once a day; do not spam rebuilds. [source: Rebrickable Downloads page, text read 2026-10-06]

  • The .sqlite file is never committed (gitignored) and never published as a download; it is bundled only inside the function.

  • To host your own copy, see docs/SELF_HOSTING.md.

6. Development

Requires Node 22.x (engines in the root package.json; node:sqlite is used and prints an experimental warning) and pnpm.

pnpm install

pnpm lint            # eslint .
pnpm typecheck       # tsc --noEmit in every package
pnpm test            # vitest in every package

# Build the SQLite snapshot (downloads Rebrickable CSVs; max one automated download per day)
pnpm --filter @mcps/lego-catalog build:snapshot

# Local dev server on http://127.0.0.1:3000/lego-catalog/mcp (PORT env overrides the port)
pnpm --filter @mcps/lego-catalog dev

# Smoke test any endpoint
node scripts/smoke-mcp.mjs --no-auth http://127.0.0.1:3000/lego-catalog/mcp   # the dev server has no token check

Tests that need the real snapshot skip when LEGO_CATALOG_DB is unset. To run everything, point it at a built snapshot:

$env:LEGO_CATALOG_DB = "C:\path\to\rebrickable.sqlite"; pnpm test

The server looks for the snapshot in LEGO_CATALOG_DB, then mcps/lego-catalog/data/rebrickable.sqlite.

MCP_AUTH_TOKEN is required by the deployed function: set it as a Sensitive environment variable for Production in Vercel (at least 32 random characters). If it is unset the function answers 503 to every request (it fails closed). The smoke test reads the same variable from the environment, never from the command line. The local dev server does not check it.

7. Security and status

See docs/SECURITY_REVIEW.md for the review and its findings. Honest status:

  • The deployed function requires Authorization: Bearer <token>. The token is compared in constant time against MCP_AUTH_TOKEN; a missing or wrong token gets 401 before the request body is read, and an unset variable gets 503.

  • The local dev server has no token check and listens on 127.0.0.1 only.

  • The maintainer's deployment is private.

  • No CORS headers are sent.

  • Data: Rebrickable (https://rebrickable.com/downloads/). Rebrickable asks to be acknowledged as the source of the data [source: Rebrickable Downloads page, text read 2026-10-06].

  • LEGO is a trademark of the LEGO Group; this project is not affiliated with or endorsed by it.

  • Rebrickable's Terms of Service (section 5.3) forbid training AI models on their content. This server serves data at inference time only. [source: Rebrickable Terms of Service, text read 2026-10-06]

  • Not covered by the texts read: whether serving data derived from the CSVs is permitted. That question is open. This is not legal advice; read Rebrickable's terms yourself before reuse.

9. License

The code is released under the MIT License, copyright (c) 2026 Marian Sanjur.

The license covers this repository's code only. The catalog data comes from Rebrickable and stays under Rebrickable's own terms (see section 8); it is not relicensed by this project.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    An MCP server that exposes authoritative, read-only Conda ecosystem metadata for AI agents, enabling package search, dependency resolution, and other packaging queries without side effects.
    6
    BSD 3-Clause
  • A
    license
    Not graded
    quality
    A
    maintenance
    This MCP server enables AI agents to manage artifacts across sessions by providing tools for searching, retrieving, and registering entries in a persistent ledger, ensuring consistency and traceability of agent outputs.
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    Read-only MCP server that exposes a design system's tokens, components, conventions, and deprecations as queryable tools, enabling agents to look up canonical values, assess change impact, and detect hardcoded value drift.
    8
    1 npm
    MIT