@coreviz/cli 1.0.12 → 1.2.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.
package/README.md CHANGED
@@ -138,6 +138,73 @@ Check login status:
138
138
  npx @coreviz/cli whoami
139
139
  ```
140
140
 
141
+ ## MCP Server (Claude Code Integration)
142
+
143
+ `@coreviz/cli` includes a built-in MCP server that exposes your CoreViz visual library as tools for Claude Code and other MCP-compatible AI agents — turning CoreViz into a **visual memory** for your AI workflows.
144
+
145
+ ### Setup
146
+
147
+ 1. Login (if you haven't already):
148
+ ```bash
149
+ npx @coreviz/cli login
150
+ ```
151
+
152
+ 2. Add to your project's `.mcp.json`:
153
+ ```json
154
+ {
155
+ "mcpServers": {
156
+ "coreviz": {
157
+ "command": "npx",
158
+ "args": ["coreviz-mcp"]
159
+ }
160
+ }
161
+ }
162
+ ```
163
+
164
+ 3. In Claude Code, run `/mcp` to confirm the server is connected.
165
+
166
+ ### Available MCP Tools
167
+
168
+ | Tool | Description |
169
+ |------|-------------|
170
+ | `list_collections` | List all collections in your workspace |
171
+ | `create_collection` | Create a new collection |
172
+ | `browse_media` | Navigate folders and list media items |
173
+ | `search_media` | Semantic search across all your media |
174
+ | `get_media` | Get full details, tags, and detected objects for an item |
175
+ | `get_tags` | Aggregate all tags across a collection |
176
+ | `find_similar` | Find visually similar images by object ID |
177
+ | `analyze_image` | Run AI vision analysis on an image URL |
178
+ | `create_folder` | Create a new folder |
179
+ | `move_item` | Move a file or folder |
180
+ | `rename_item` | Rename a file or folder |
181
+ | `add_tag` | Add a tag to a media item |
182
+ | `remove_tag` | Remove a tag from a media item |
183
+ | `upload_media` | Upload a local photo or video file to a collection |
184
+
185
+ ### Local development override
186
+
187
+ ```json
188
+ {
189
+ "mcpServers": {
190
+ "coreviz": {
191
+ "command": "node",
192
+ "args": ["/path/to/@coreviz/cli/bin/mcp.js"],
193
+ "env": {
194
+ "COREVIZ_API_URL": "http://localhost:3000"
195
+ }
196
+ }
197
+ }
198
+ }
199
+ ```
200
+
201
+ You can also authenticate via environment variable instead of `coreviz login`:
202
+ ```bash
203
+ COREVIZ_API_KEY=your_key npx coreviz-mcp
204
+ ```
205
+
206
+ ---
207
+
141
208
  ## Development
142
209
 
143
210
  1. Install dependencies:
@@ -149,4 +216,9 @@ npx @coreviz/cli whoami
149
216
  2. Run local CLI:
150
217
  ```bash
151
218
  node bin/cli.js --help
219
+ ```
220
+
221
+ 3. Run local MCP server:
222
+ ```bash
223
+ node bin/mcp.js
152
224
  ```
package/bin/mcp.js ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CoreViz MCP Server
4
+ *
5
+ * Exposes your CoreViz visual library as tools for Claude Code and other MCP clients.
6
+ * Authentication is read from the session stored by `coreviz login`.
7
+ *
8
+ * Usage (after `coreviz login`):
9
+ * Add to .mcp.json:
10
+ * {
11
+ * "mcpServers": {
12
+ * "coreviz": { "command": "npx", "args": ["coreviz-mcp"] }
13
+ * }
14
+ * }
15
+ */
16
+
17
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
18
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
19
+ import Conf from 'conf';
20
+ import { CoreViz } from '@coreviz/sdk';
21
+ import { registerTools } from '../lib/mcp-tools.js';
22
+
23
+ const config = new Conf({ projectName: 'coreviz-cli' });
24
+
25
+ async function main() {
26
+ // Read stored session from `coreviz login`
27
+ const session = config.get('session');
28
+ const token = session?.access_token;
29
+
30
+ // Also support explicit env var overrides for CI/scripting
31
+ const apiKey = process.env.COREVIZ_API_KEY;
32
+ const baseUrl = process.env.COREVIZ_API_URL || 'https://lab.coreviz.io';
33
+
34
+ if (!token && !apiKey) {
35
+ process.stderr.write(
36
+ '[coreviz-mcp] Not authenticated. Run `coreviz login` first, ' +
37
+ 'or set COREVIZ_API_KEY environment variable.\n'
38
+ );
39
+ process.exit(1);
40
+ }
41
+
42
+ const sdk = new CoreViz({
43
+ ...(token ? { token } : { apiKey }),
44
+ baseUrl,
45
+ });
46
+
47
+ const server = new McpServer({
48
+ name: 'coreviz',
49
+ version: '1.0.0',
50
+ });
51
+
52
+ registerTools(server, sdk);
53
+
54
+ const transport = new StdioServerTransport();
55
+ await server.connect(transport);
56
+
57
+ process.stderr.write('[coreviz-mcp] Server ready\n');
58
+ }
59
+
60
+ main().catch((err) => {
61
+ process.stderr.write(`[coreviz-mcp] Fatal error: ${err?.message || err}\n`);
62
+ process.exit(1);
63
+ });
@@ -0,0 +1,225 @@
1
+ /**
2
+ * CoreViz MCP tool definitions.
3
+ * Each tool maps 1:1 to a @coreviz/sdk method.
4
+ */
5
+
6
+ import { z } from 'zod';
7
+
8
+ /** Build a text result object for MCP */
9
+ function text(value) {
10
+ return { content: [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }] };
11
+ }
12
+
13
+ /** Wrap an async tool handler with consistent error handling */
14
+ function safe(fn) {
15
+ return async (args) => {
16
+ try {
17
+ return await fn(args);
18
+ } catch (err) {
19
+ return { content: [{ type: 'text', text: `Error: ${err?.message || String(err)}` }], isError: true };
20
+ }
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Register all CoreViz tools on the given MCP server.
26
+ * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
27
+ * @param {import('@coreviz/sdk').CoreViz} sdk
28
+ */
29
+ export function registerTools(server, sdk) {
30
+
31
+ // ── Read-only tools ───────────────────────────────────────────────────────
32
+
33
+ server.tool(
34
+ 'list_collections',
35
+ 'List all collections in your CoreViz workspace. Returns collection IDs and names.',
36
+ {},
37
+ safe(async () => {
38
+ const collections = await sdk.collections.list();
39
+ return text(collections);
40
+ })
41
+ );
42
+
43
+ server.tool(
44
+ 'create_collection',
45
+ 'Create a new collection in your CoreViz workspace.',
46
+ {
47
+ name: z.string().describe('Name for the new collection'),
48
+ icon: z.string().optional().describe('Optional icon for the collection (emoji or icon name)'),
49
+ },
50
+ safe(async ({ name, icon }) => {
51
+ const collection = await sdk.collections.create(name, icon);
52
+ return text(collection);
53
+ })
54
+ );
55
+
56
+ server.tool(
57
+ 'browse_media',
58
+ 'Browse or list media items and folders inside a collection. Use the ltree path to navigate subfolders (e.g. "collectionId.folderId"). Returns file/folder IDs, names, types, blob URLs, and metadata.',
59
+ {
60
+ collectionId: z.string().describe('The collection ID to browse (from list_collections)'),
61
+ path: z.string().optional().describe('ltree path to list (e.g. "collectionId" for root, "collectionId.folderId" for a subfolder). Defaults to the collection root.'),
62
+ limit: z.number().optional().default(50).describe('Max number of items to return (default 50)'),
63
+ offset: z.number().optional().default(0).describe('Pagination offset'),
64
+ type: z.enum(['image', 'video', 'folder', 'all']).optional().describe('Filter by item type'),
65
+ dateFrom: z.string().optional().describe('Filter by creation date from (YYYY-MM-DD)'),
66
+ dateTo: z.string().optional().describe('Filter by creation date to (YYYY-MM-DD)'),
67
+ sortBy: z.string().optional().describe('Sort field: name, createdAt, type'),
68
+ sortDirection: z.enum(['asc', 'desc']).optional().describe('Sort direction'),
69
+ },
70
+ safe(async ({ collectionId, ...opts }) => {
71
+ const result = await sdk.media.browse(collectionId, opts);
72
+ return text(result);
73
+ })
74
+ );
75
+
76
+ server.tool(
77
+ 'search_media',
78
+ 'Semantically search across all media in your CoreViz workspace using natural language. Returns ranked results with image URLs, detected objects, and metadata. Use this to find specific images, scenes, or subjects.',
79
+ {
80
+ query: z.string().describe('Natural language search query (e.g. "red shoes", "sunset over mountains", "person wearing glasses")'),
81
+ limit: z.number().optional().default(10).describe('Max number of results to return (default 10, max 50)'),
82
+ },
83
+ safe(async ({ query, limit }) => {
84
+ const results = await sdk.media.search(query, { limit });
85
+ return text(results);
86
+ })
87
+ );
88
+
89
+ server.tool(
90
+ 'get_media',
91
+ 'Get full details for a specific media item: blob URL, dimensions, tags, detected objects, version history, and metadata.',
92
+ {
93
+ mediaId: z.string().describe('The media item ID (from browse_media or search_media results)'),
94
+ },
95
+ safe(async ({ mediaId }) => {
96
+ const media = await sdk.media.get(mediaId);
97
+ return text(media);
98
+ })
99
+ );
100
+
101
+ server.tool(
102
+ 'get_tags',
103
+ 'Get all tag groups and their values aggregated across an entire collection. Useful for understanding how a collection is categorized.',
104
+ {
105
+ collectionId: z.string().describe('The collection ID (from list_collections)'),
106
+ },
107
+ safe(async ({ collectionId }) => {
108
+ const tags = await sdk.tags.list(collectionId);
109
+ return text(tags);
110
+ })
111
+ );
112
+
113
+ server.tool(
114
+ 'find_similar',
115
+ 'Find media items that are visually similar to a specific detected object. Provide an object ID from a previous get_media or search_media result. Useful for face recognition, product matching, or pattern finding.',
116
+ {
117
+ collectionId: z.string().describe('The collection ID to search within'),
118
+ objectId: z.string().describe('The object ID from a detected object (from get_media frames[].objects[].id or search_media objects[].id)'),
119
+ limit: z.number().optional().default(10).describe('Max number of similar items to return'),
120
+ model: z.string().optional().describe('Similarity model to use: "faces", "objects", or "shoeprints"'),
121
+ },
122
+ safe(async ({ collectionId, objectId, limit, model }) => {
123
+ const result = await sdk.media.findSimilar(collectionId, objectId, { limit, model });
124
+ return text(result);
125
+ })
126
+ );
127
+
128
+ // ── Write tools ───────────────────────────────────────────────────────────
129
+
130
+ server.tool(
131
+ 'analyze_image',
132
+ 'Analyze an image using AI vision. Provide a blob URL (from browse_media or search_media results). Returns a detailed description of the image contents.',
133
+ {
134
+ imageUrl: z.string().describe('The blob URL of the image to analyze (must be a real URL from browse_media or search_media, never guessed)'),
135
+ prompt: z.string().optional().default('Describe this image in detail.').describe('Optional question or prompt to ask about the image'),
136
+ },
137
+ safe(async ({ imageUrl, prompt }) => {
138
+ const description = await sdk.describe(imageUrl, { prompt });
139
+ return text(description);
140
+ })
141
+ );
142
+
143
+ server.tool(
144
+ 'create_folder',
145
+ 'Create a new folder inside a collection.',
146
+ {
147
+ collectionId: z.string().describe('The collection ID where the folder will be created'),
148
+ name: z.string().describe('Name of the new folder'),
149
+ path: z.string().optional().describe('Parent ltree path for the folder (e.g. "collectionId" for root, "collectionId.parentFolderId" for a subfolder). Defaults to collection root.'),
150
+ },
151
+ safe(async ({ collectionId, name, path }) => {
152
+ const folder = await sdk.folders.create(collectionId, name, path);
153
+ return text(folder);
154
+ })
155
+ );
156
+
157
+ server.tool(
158
+ 'move_item',
159
+ 'Move a media item or folder to a different location within the same collection. Both sourceId and destinationPath must come from previous browse_media results — never construct paths manually.',
160
+ {
161
+ mediaId: z.string().describe('The ID of the media item or folder to move'),
162
+ destinationPath: z.string().describe('The ltree path of the destination folder (from browse_media results, e.g. "collectionId.folderId")'),
163
+ },
164
+ safe(async ({ mediaId, destinationPath }) => {
165
+ const result = await sdk.media.move(mediaId, destinationPath);
166
+ return text(result);
167
+ })
168
+ );
169
+
170
+ server.tool(
171
+ 'rename_item',
172
+ 'Rename a media item or folder.',
173
+ {
174
+ mediaId: z.string().describe('The ID of the media item to rename'),
175
+ name: z.string().describe('The new name for the item'),
176
+ },
177
+ safe(async ({ mediaId, name }) => {
178
+ const media = await sdk.media.rename(mediaId, name);
179
+ return text(media);
180
+ })
181
+ );
182
+
183
+ server.tool(
184
+ 'add_tag',
185
+ 'Add a tag to a media item. Tags are organized as label (group) + value pairs, e.g. label="color", value="red".',
186
+ {
187
+ mediaId: z.string().describe('The media item ID'),
188
+ label: z.string().describe('Tag group name (e.g. "color", "category", "quality")'),
189
+ value: z.string().describe('Tag value (e.g. "red", "product", "high")'),
190
+ },
191
+ safe(async ({ mediaId, label, value }) => {
192
+ await sdk.media.addTag(mediaId, label, value);
193
+ return text({ success: true, mediaId, tag: { label, value } });
194
+ })
195
+ );
196
+
197
+ server.tool(
198
+ 'remove_tag',
199
+ 'Remove a specific tag from a media item.',
200
+ {
201
+ mediaId: z.string().describe('The media item ID'),
202
+ label: z.string().describe('Tag group name to remove'),
203
+ value: z.string().describe('Tag value to remove'),
204
+ },
205
+ safe(async ({ mediaId, label, value }) => {
206
+ await sdk.media.removeTag(mediaId, label, value);
207
+ return text({ success: true, mediaId, removed: { label, value } });
208
+ })
209
+ );
210
+
211
+ server.tool(
212
+ 'upload_media',
213
+ 'Upload a local photo or video file to a CoreViz collection. Provide the absolute path to the file and the target collection ID. Optionally specify a folder path and a custom name.',
214
+ {
215
+ filePath: z.string().describe('Absolute path to the local file to upload (e.g. /Users/you/photo.jpg)'),
216
+ collectionId: z.string().describe('The collection ID to upload into (from list_collections)'),
217
+ path: z.string().optional().describe('ltree folder path to upload into (e.g. "collectionId.folderId"). Defaults to collection root.'),
218
+ name: z.string().optional().describe('Custom name for the file in CoreViz. Defaults to the original filename.'),
219
+ },
220
+ safe(async ({ filePath, collectionId, path, name }) => {
221
+ const result = await sdk.media.upload(filePath, { collectionId, path, name });
222
+ return text(result);
223
+ })
224
+ );
225
+ }
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@coreviz/cli",
3
- "version": "1.0.12",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "CoreViz CLI tool",
6
6
  "main": "index.js",
7
7
  "bin": {
8
- "coreviz": "./bin/cli.js"
8
+ "coreviz": "./bin/cli.js",
9
+ "coreviz-mcp": "./bin/mcp.js"
9
10
  },
10
11
  "scripts": {
11
12
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -27,7 +28,8 @@
27
28
  "homepage": "https://github.com/CoreViz/cli#readme",
28
29
  "dependencies": {
29
30
  "@clack/prompts": "^0.11.0",
30
- "@coreviz/sdk": "^1.0.11",
31
+ "@coreviz/sdk": "^1.1.0",
32
+ "@modelcontextprotocol/sdk": "^1.27.1",
31
33
  "better-auth": "^1.4.2",
32
34
  "better-sqlite3": "^12.4.6",
33
35
  "chalk": "^5.6.2",