@iflow-mcp/nazarlysyi-brickscope 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +91 -0
  3. package/dist/cli/commands/config.js +81 -0
  4. package/dist/cli/commands/health.js +27 -0
  5. package/dist/cli/commands/identify.js +72 -0
  6. package/dist/cli/commands/mcp.js +14 -0
  7. package/dist/cli/commands/minifig.js +57 -0
  8. package/dist/cli/commands/part.js +59 -0
  9. package/dist/cli/commands/set.js +57 -0
  10. package/dist/cli/index.js +30 -0
  11. package/dist/cli/output.js +84 -0
  12. package/dist/core/brickognize/client.js +39 -0
  13. package/dist/core/brickognize/mappers.js +53 -0
  14. package/dist/core/brickognize/types.js +3 -0
  15. package/dist/core/cache/index.js +30 -0
  16. package/dist/core/cache/memory.js +15 -0
  17. package/dist/core/cache/sqlite.js +33 -0
  18. package/dist/core/cache/types.js +2 -0
  19. package/dist/core/concurrency.js +18 -0
  20. package/dist/core/config.js +43 -0
  21. package/dist/core/image.js +45 -0
  22. package/dist/core/rebrickable/client.js +91 -0
  23. package/dist/core/rebrickable/minifigDetails.js +26 -0
  24. package/dist/core/rebrickable/partDetails.js +80 -0
  25. package/dist/core/rebrickable/setDetails.js +47 -0
  26. package/dist/core/rebrickable/types.js +3 -0
  27. package/dist/core/utils/errors.js +51 -0
  28. package/dist/mcp/index.js +9 -0
  29. package/dist/mcp/server.js +56 -0
  30. package/dist/mcp/tools/batchPredict.js +55 -0
  31. package/dist/mcp/tools/cacheTools.js +24 -0
  32. package/dist/mcp/tools/health.js +19 -0
  33. package/dist/mcp/tools/minifigDetails.js +26 -0
  34. package/dist/mcp/tools/partDetails.js +83 -0
  35. package/dist/mcp/tools/predict.js +36 -0
  36. package/dist/mcp/tools/setDetails.js +30 -0
  37. package/dist/mcp/tools/shared.js +28 -0
  38. package/package.json +1 -0
@@ -0,0 +1,55 @@
1
+ import { z } from "zod";
2
+ import { predict } from "../../core/brickognize/client.js";
3
+ import { mapPredictionResult } from "../../core/brickognize/mappers.js";
4
+ import { formatToolError } from "../../core/utils/errors.js";
5
+ import { runWithConcurrencyLimit } from "../../core/concurrency.js";
6
+ import { PREDICT_ENDPOINTS, resolveImage, TOOL_ANNOTATIONS, toolSuccess } from "./shared.js";
7
+ const CONCURRENCY_LIMIT = 5;
8
+ const MAX_BATCH_SIZE = 20;
9
+ async function processSingleImage(imagePath, endpoint, includeRaw) {
10
+ try {
11
+ const { blob, filename } = await resolveImage({ imagePath });
12
+ const raw = await predict(endpoint, blob, filename);
13
+ const result = mapPredictionResult(raw, includeRaw);
14
+ return { imagePath, status: "success", result };
15
+ }
16
+ catch (err) {
17
+ return { imagePath, status: "error", error: formatToolError(err) };
18
+ }
19
+ }
20
+ export function registerBatchIdentifyTool(server) {
21
+ server.registerTool("brickognize_batch_identify", {
22
+ title: "Batch Identify LEGO Items",
23
+ description: "Identify multiple LEGO items from local image files in a single call. " +
24
+ "Processes all images in parallel and returns an array of results. " +
25
+ "Use this when the user provides a folder of photos or multiple image paths. " +
26
+ "Accepts 1–20 image paths per call.\n\n" +
27
+ "When type='part', color prediction is included automatically in each result's predictedColors field.",
28
+ inputSchema: {
29
+ imagePaths: z
30
+ .array(z.string())
31
+ .min(1)
32
+ .max(MAX_BATCH_SIZE)
33
+ .describe(`Array of absolute paths to local image files (JPEG, PNG, or WebP). Max ${MAX_BATCH_SIZE} images per call.`),
34
+ type: z
35
+ .enum(["general", "part", "set", "fig"])
36
+ .default("part")
37
+ .describe("Type of identification: 'part' for single bricks/elements, 'set' for assembled sets or boxes, 'fig' for minifigures, 'general' when unknown."),
38
+ includeRaw: z
39
+ .boolean()
40
+ .default(false)
41
+ .describe("When true, includes the raw Brickognize API response in each result."),
42
+ },
43
+ annotations: TOOL_ANNOTATIONS,
44
+ }, async ({ imagePaths, type, includeRaw }) => {
45
+ const endpoint = PREDICT_ENDPOINTS[type];
46
+ const tasks = imagePaths.map((imagePath) => () => processSingleImage(imagePath, endpoint, includeRaw));
47
+ const results = await runWithConcurrencyLimit(tasks, CONCURRENCY_LIMIT);
48
+ const succeeded = results.filter((r) => r.status === "success").length;
49
+ const failed = results.length - succeeded;
50
+ const summary = `Batch complete: ${succeeded}/${results.length} succeeded` +
51
+ (failed > 0 ? `, ${failed} failed.` : ".");
52
+ return toolSuccess(summary, JSON.stringify(results, null, 2));
53
+ });
54
+ }
55
+ //# sourceMappingURL=batchPredict.js.map
@@ -0,0 +1,24 @@
1
+ import { toolError, toolSuccess } from "./shared.js";
2
+ export function registerCacheClearTool(server, cache) {
3
+ server.registerTool("brickognize_cache_clear", {
4
+ title: "Clear Rebrickable Cache",
5
+ description: "Clear the local cache of Rebrickable API responses (part details, set inventories, etc.). " +
6
+ "Use when you want to force fresh data from Rebrickable, e.g. after a long time or if data looks stale. " +
7
+ "Returns the number of entries removed.",
8
+ annotations: {
9
+ readOnlyHint: false,
10
+ destructiveHint: true,
11
+ idempotentHint: true,
12
+ openWorldHint: false,
13
+ },
14
+ }, async () => {
15
+ try {
16
+ const count = cache.clear();
17
+ return toolSuccess(`Cache cleared. ${count} entr${count === 1 ? "y" : "ies"} removed.`);
18
+ }
19
+ catch (error) {
20
+ return toolError(error);
21
+ }
22
+ });
23
+ }
24
+ //# sourceMappingURL=cacheTools.js.map
@@ -0,0 +1,19 @@
1
+ import { checkHealth } from "../../core/brickognize/client.js";
2
+ import { TOOL_ANNOTATIONS, toolError, toolSuccess } from "./shared.js";
3
+ export function registerHealthTool(server) {
4
+ server.registerTool("brickognize_health", {
5
+ title: "Brickognize Health Check",
6
+ description: "Check whether the Brickognize image recognition API is online and responsive. " +
7
+ "Call this before recognition if you suspect the service might be down. Takes no parameters.",
8
+ annotations: TOOL_ANNOTATIONS,
9
+ }, async () => {
10
+ try {
11
+ const health = await checkHealth();
12
+ return toolSuccess(JSON.stringify(health, null, 2));
13
+ }
14
+ catch (error) {
15
+ return toolError(error);
16
+ }
17
+ });
18
+ }
19
+ //# sourceMappingURL=health.js.map
@@ -0,0 +1,26 @@
1
+ import { z } from "zod";
2
+ import { fetchMinifigDetails, buildMinifigSummary } from "../../core/rebrickable/minifigDetails.js";
3
+ import { TOOL_ANNOTATIONS, toolError, toolSuccess } from "./shared.js";
4
+ export function registerMinifigDetailsTool(server) {
5
+ server.registerTool("brickognize_minifig_details", {
6
+ title: "LEGO Minifigure Details",
7
+ description: "Get detailed information about a LEGO minifigure by its ID: name, parts count, " +
8
+ "and which sets contain this minifigure. " +
9
+ "Use after brickognize_identify_fig to enrich results, or directly with a known minifigure ID.\n\n" +
10
+ 'Accepts minifigure IDs like "fig-000001" or set-style IDs.',
11
+ inputSchema: {
12
+ minifigId: z.string().describe('LEGO minifigure ID, e.g. "fig-000001".'),
13
+ },
14
+ annotations: TOOL_ANNOTATIONS,
15
+ }, async (input) => {
16
+ try {
17
+ const result = await fetchMinifigDetails(input.minifigId);
18
+ const summary = buildMinifigSummary(result);
19
+ return toolSuccess(summary, JSON.stringify(result, null, 2));
20
+ }
21
+ catch (error) {
22
+ return toolError(error);
23
+ }
24
+ });
25
+ }
26
+ //# sourceMappingURL=minifigDetails.js.map
@@ -0,0 +1,83 @@
1
+ import { z } from "zod";
2
+ import { fetchPartDetails, buildPartSummary, normalizeColorName, matchColorByName, } from "../../core/rebrickable/partDetails.js";
3
+ import { formatToolError } from "../../core/utils/errors.js";
4
+ import { TOOL_ANNOTATIONS, toolError, toolSuccess } from "./shared.js";
5
+ // Re-export for tests
6
+ export { normalizeColorName, matchColorByName, buildPartSummary as buildSingleSummary };
7
+ export function registerPartDetailsTool(server) {
8
+ server.registerTool("brickognize_part_details", {
9
+ title: "LEGO Part Details",
10
+ description: "Get detailed information about a LEGO part by its ID: available colors, " +
11
+ "and which sets contain this part (appears in). " +
12
+ "Use after brickognize_identify_part to enrich results, or directly with a known part number.\n\n" +
13
+ "When colorName is provided (e.g. from predictedColors in identify results), " +
14
+ "returns sets only for that specific color — much faster and more precise.\n" +
15
+ "Without colorName, returns sets for the top 5 most popular colors.\n\n" +
16
+ "For multiple parts at once, use brickognize_batch_part_details instead.",
17
+ inputSchema: {
18
+ partId: z.string().describe('LEGO part number, e.g. "3001" for Brick 2x4.'),
19
+ colorName: z
20
+ .string()
21
+ .describe('Optional color name to filter by (e.g. "Black"). ' +
22
+ "Pass the predicted color name from brickognize_identify_part to get sets for that exact color.")
23
+ .optional(),
24
+ },
25
+ annotations: TOOL_ANNOTATIONS,
26
+ }, async (input) => {
27
+ try {
28
+ const result = await fetchPartDetails(input.partId, input.colorName);
29
+ const summary = buildPartSummary(result);
30
+ return toolSuccess(summary, JSON.stringify(result, null, 2));
31
+ }
32
+ catch (error) {
33
+ return toolError(error);
34
+ }
35
+ });
36
+ }
37
+ export function registerBatchPartDetailsTool(server) {
38
+ server.registerTool("brickognize_batch_part_details", {
39
+ title: "Batch LEGO Part Details",
40
+ description: "Get details for multiple LEGO parts in a single call: colors, and which sets contain each part.\n\n" +
41
+ "Ideal workflow: call brickognize_batch_identify first, then pass all identified parts " +
42
+ "with their predicted colors to this tool in one call.\n\n" +
43
+ "Each entry needs a partId and optional colorName for targeted color lookup. " +
44
+ "Results are returned in the same order as the input.",
45
+ inputSchema: {
46
+ parts: z
47
+ .array(z.object({
48
+ partId: z.string().describe("LEGO part number"),
49
+ colorName: z
50
+ .string()
51
+ .describe('Color name from predictedColors (e.g. "Black")')
52
+ .optional(),
53
+ }))
54
+ .min(1)
55
+ .max(20)
56
+ .describe("Array of parts to look up. Max 20 per call."),
57
+ },
58
+ annotations: TOOL_ANNOTATIONS,
59
+ }, async ({ parts }) => {
60
+ try {
61
+ const results = [];
62
+ // Process sequentially due to Rebrickable rate limiting (1 req/sec)
63
+ for (const entry of parts) {
64
+ try {
65
+ const result = await fetchPartDetails(entry.partId, entry.colorName);
66
+ results.push({ partId: entry.partId, status: "success", result });
67
+ }
68
+ catch (err) {
69
+ results.push({ partId: entry.partId, status: "error", error: formatToolError(err) });
70
+ }
71
+ }
72
+ const succeeded = results.filter((r) => r.status === "success").length;
73
+ const failed = results.length - succeeded;
74
+ const summary = `Batch part details: ${succeeded}/${results.length} succeeded` +
75
+ (failed > 0 ? `, ${failed} failed.` : ".");
76
+ return toolSuccess(summary, JSON.stringify(results, null, 2));
77
+ }
78
+ catch (error) {
79
+ return toolError(error);
80
+ }
81
+ });
82
+ }
83
+ //# sourceMappingURL=partDetails.js.map
@@ -0,0 +1,36 @@
1
+ import { predict } from "../../core/brickognize/client.js";
2
+ import { mapPredictionResult } from "../../core/brickognize/mappers.js";
3
+ import { imageInputSchema, PREDICT_ENDPOINTS, resolveImage, TOOL_ANNOTATIONS, toolError, toolSuccess, } from "./shared.js";
4
+ function createPredictTool(server, name, title, description, endpoint) {
5
+ server.registerTool(name, { title, description, inputSchema: imageInputSchema, annotations: TOOL_ANNOTATIONS }, async (input) => {
6
+ try {
7
+ const { blob, filename } = await resolveImage(input);
8
+ const raw = await predict(endpoint, blob, filename);
9
+ const result = mapPredictionResult(raw, input.includeRaw);
10
+ return toolSuccess(result.summary, JSON.stringify(result, null, 2));
11
+ }
12
+ catch (error) {
13
+ return toolError(error);
14
+ }
15
+ });
16
+ }
17
+ export function registerPredictTools(server) {
18
+ createPredictTool(server, "brickognize_identify", "Identify LEGO Item", "Identify any LEGO item (part, set, minifigure, or sticker) from a photograph. " +
19
+ "Use this when the item type is unknown or the image may contain multiple types.\n\n" +
20
+ "Provide imagePath — absolute path to a local image file (JPEG, PNG, or WebP).\n" +
21
+ "Returns top matches with confidence scores, IDs, names, categories, and links to BrickLink/BrickOwl.", PREDICT_ENDPOINTS.general);
22
+ createPredictTool(server, "brickognize_identify_part", "Identify LEGO Part", "Identify a specific LEGO part/brick/element from a photograph. " +
23
+ "Use instead of brickognize_identify when you know the image shows a single LEGO piece for more accurate results.\n\n" +
24
+ "Provide imagePath — absolute path to a local image file (JPEG, PNG, or WebP).\n" +
25
+ "Automatically detects the part's color (returned in predictedColors).\n" +
26
+ "Returns matched parts with IDs, names, confidence scores, predicted colors, and links.", PREDICT_ENDPOINTS.part);
27
+ createPredictTool(server, "brickognize_identify_set", "Identify LEGO Set", "Identify a LEGO set from a photograph of its box, assembled model, or instructions. " +
28
+ "Use instead of brickognize_identify when you know the image shows a LEGO set.\n\n" +
29
+ "Provide imagePath — absolute path to a local image file (JPEG, PNG, or WebP).\n" +
30
+ "Returns matched sets with set numbers, names, confidence scores, and links.", PREDICT_ENDPOINTS.set);
31
+ createPredictTool(server, "brickognize_identify_fig", "Identify LEGO Minifigure", "Identify a LEGO minifigure from a photograph. " +
32
+ "Use instead of brickognize_identify when you know the image shows a minifigure.\n\n" +
33
+ "Provide imagePath — absolute path to a local image file (JPEG, PNG, or WebP).\n" +
34
+ "Returns matched minifigures with IDs, names, confidence scores, and links.", PREDICT_ENDPOINTS.fig);
35
+ }
36
+ //# sourceMappingURL=predict.js.map
@@ -0,0 +1,30 @@
1
+ import { z } from "zod";
2
+ import { fetchSetDetails, buildSetSummary, normalizeSetNum, } from "../../core/rebrickable/setDetails.js";
3
+ import { TOOL_ANNOTATIONS, toolError, toolSuccess } from "./shared.js";
4
+ // Re-export for tests
5
+ export { normalizeSetNum };
6
+ export function registerSetDetailsTool(server) {
7
+ server.registerTool("brickognize_set_details", {
8
+ title: "LEGO Set Details",
9
+ description: "Get detailed information about a LEGO set by its number: year, theme, piece count, " +
10
+ "and full parts inventory. " +
11
+ "Use after brickognize_identify_set to enrich results, or directly with a known set number.\n\n" +
12
+ 'The "-1" suffix is added automatically if missing (e.g. "75192" → "75192-1").',
13
+ inputSchema: {
14
+ setId: z
15
+ .string()
16
+ .describe('LEGO set number, e.g. "75192-1" or "75192". The "-1" suffix is added automatically.'),
17
+ },
18
+ annotations: TOOL_ANNOTATIONS,
19
+ }, async (input) => {
20
+ try {
21
+ const result = await fetchSetDetails(input.setId);
22
+ const summary = buildSetSummary(result);
23
+ return toolSuccess(summary, JSON.stringify(result, null, 2));
24
+ }
25
+ catch (error) {
26
+ return toolError(error);
27
+ }
28
+ });
29
+ }
30
+ //# sourceMappingURL=setDetails.js.map
@@ -0,0 +1,28 @@
1
+ import { z } from "zod";
2
+ import { formatToolError } from "../../core/utils/errors.js";
3
+ export { PREDICT_ENDPOINTS, resolveImage } from "../../core/image.js";
4
+ /** Build a successful tool response with one or more text content blocks. */
5
+ export function toolSuccess(...texts) {
6
+ return { content: texts.map((text) => ({ type: "text", text })) };
7
+ }
8
+ /** Build an error tool response from a caught exception. */
9
+ export function toolError(error) {
10
+ return {
11
+ isError: true,
12
+ content: [{ type: "text", text: formatToolError(error) }],
13
+ };
14
+ }
15
+ export const TOOL_ANNOTATIONS = {
16
+ readOnlyHint: true,
17
+ destructiveHint: false,
18
+ idempotentHint: true,
19
+ openWorldHint: true,
20
+ };
21
+ export const imageInputSchema = {
22
+ imagePath: z.string().describe("Absolute path to a local image file (JPEG, PNG, or WebP)."),
23
+ includeRaw: z
24
+ .boolean()
25
+ .describe("When true, includes the raw Brickognize API response alongside formatted results. Useful for debugging.")
26
+ .default(false),
27
+ };
28
+ //# sourceMappingURL=shared.js.map
package/package.json ADDED
@@ -0,0 +1 @@
1
+ {"name": "@iflow-mcp/nazarlysyi-brickscope", "version": "0.1.0", "description": "CLI and MCP server for LEGO recognition — identify parts, sets, and minifigures from images", "type": "module", "main": "dist/mcp/index.js", "bin": {"iflow-mcp-nazarlysyi-brickscope": "dist/cli/index.js"}, "files": ["dist/**/*.js", "README.md", "LICENSE"], "scripts": {"build": "tsc", "start": "node dist/cli/index.js", "start:mcp": "node dist/mcp/index.js", "dev": "tsc --watch", "test": "vitest run", "test:watch": "vitest", "lint": "eslint src/", "lint:fix": "eslint src/ --fix", "format": "prettier --write .", "format:check": "prettier --check ."}, "keywords": ["mcp", "lego", "brickscope", "brickognize", "image-recognition", "cli", "brick", "rebrickable"], "license": "MIT", "dependencies": {"@modelcontextprotocol/sdk": "^1.27.1", "better-sqlite3": "^9.6.0", "commander": "^14.0.3", "zod": "^3.24.0"}, "devDependencies": {"@eslint/js": "^10.0.1", "@types/better-sqlite3": "^7.6.13", "@types/node": "^22.0.0", "eslint": "^10.0.3", "eslint-config-prettier": "^10.1.8", "prettier": "^3.8.1", "typescript": "^5.7.0", "typescript-eslint": "^8.57.1", "vitest": "^4.1.1"}, "engines": {"node": ">=18.0.0"}}