ultralytics-mcp 0.1.6 → 0.1.7

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
@@ -19,8 +19,6 @@ dataset uploads.
19
19
  - [Get API Key](#get-api-key)
20
20
  - [Environment Variables](#environment-variables)
21
21
  - [Installation](#installation)
22
- - [Claude Code](#claude-code)
23
- - [Codex](#codex)
24
22
  - [Verify Setup](#verify-setup)
25
23
  - [What You Can Do](#what-you-can-do)
26
24
  - [Tools](#tools)
@@ -61,7 +59,7 @@ Works in many MCP clients that accept JSON stdio server definitions.
61
59
  "mcpServers": {
62
60
  "ultralytics": {
63
61
  "command": "npx",
64
- "args": ["-y", "ultralytics-mcp"],
62
+ "args": ["-y", "ultralytics-mcp@latest"],
65
63
  "env": {
66
64
  "ULTRALYTICS_API_KEY": "ul_your_api_key_here"
67
65
  }
@@ -70,12 +68,34 @@ Works in many MCP clients that accept JSON stdio server definitions.
70
68
  }
71
69
  ```
72
70
 
73
- ### Claude Code
71
+ <details>
72
+ <summary>Antigravity</summary>
73
+
74
+ Add via the Antigravity settings or by updating your configuration file:
75
+
76
+ ```json
77
+ {
78
+ "mcpServers": {
79
+ "ultralytics": {
80
+ "command": "npx",
81
+ "args": ["-y", "ultralytics-mcp@latest"],
82
+ "env": {
83
+ "ULTRALYTICS_API_KEY": "ul_your_api_key_here"
84
+ }
85
+ }
86
+ }
87
+ }
88
+ ```
89
+
90
+ </details>
91
+
92
+ <details>
93
+ <summary>Claude Code</summary>
74
94
 
75
95
  Add server with Claude Code CLI:
76
96
 
77
97
  ```bash
78
- claude mcp add ultralytics --env ULTRALYTICS_API_KEY=ul_your_api_key_here -- npx -y ultralytics-mcp
98
+ claude mcp add ultralytics --env ULTRALYTICS_API_KEY=ul_your_api_key_here -- npx -y ultralytics-mcp@latest
79
99
  ```
80
100
 
81
101
  Or add a project-scoped server in repo-root `.mcp.json`:
@@ -85,7 +105,7 @@ Or add a project-scoped server in repo-root `.mcp.json`:
85
105
  "mcpServers": {
86
106
  "ultralytics": {
87
107
  "command": "npx",
88
- "args": ["-y", "ultralytics-mcp"],
108
+ "args": ["-y", "ultralytics-mcp@latest"],
89
109
  "env": {
90
110
  "ULTRALYTICS_API_KEY": "ul_your_api_key_here"
91
111
  }
@@ -94,12 +114,22 @@ Or add a project-scoped server in repo-root `.mcp.json`:
94
114
  }
95
115
  ```
96
116
 
97
- ### Codex
117
+ </details>
118
+
119
+ <details>
120
+ <summary>Claude Desktop</summary>
121
+
122
+ Follow the MCP install [guide](https://modelcontextprotocol.io/quickstart/user), use the standard config above.
123
+
124
+ </details>
125
+
126
+ <details>
127
+ <summary>Codex</summary>
98
128
 
99
129
  Add server with Codex CLI:
100
130
 
101
131
  ```bash
102
- codex mcp add ultralytics --env ULTRALYTICS_API_KEY=ul_your_api_key_here -- npx -y ultralytics-mcp
132
+ codex mcp add ultralytics --env ULTRALYTICS_API_KEY=ul_your_api_key_here -- npx -y ultralytics-mcp@latest
103
133
  ```
104
134
 
105
135
  Or add it directly to `~/.codex/config.toml`:
@@ -107,12 +137,62 @@ Or add it directly to `~/.codex/config.toml`:
107
137
  ```toml
108
138
  [mcp_servers.ultralytics]
109
139
  command = "npx"
110
- args = ["-y", "ultralytics-mcp"]
140
+ args = ["-y", "ultralytics-mcp@latest"]
111
141
 
112
142
  [mcp_servers.ultralytics.env]
113
143
  ULTRALYTICS_API_KEY = "ul_your_api_key_here"
114
144
  ```
115
145
 
146
+ </details>
147
+
148
+ <details>
149
+ <summary>Cursor</summary>
150
+
151
+ #### Click the button to install:
152
+
153
+ [<img src="https://cursor.com/deeplink/mcp-install-dark.svg" alt="Install in Cursor">](https://cursor.com/en/install-mcp?name=ultralytics&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsInVsdHJhbHl0aWNzLW1jcEBsYXRlc3QiXSwiZW52Ijp7IlVMVFJBTFlUSUNTX0FQSV9LRVkiOiJ1bF95b3VyX2FwaV9rZXlfaGVyZSJ9fQ%3D%3D)
154
+
155
+ > **Important**
156
+ > The install button writes a placeholder key. After installing, open your Cursor MCP config and replace `ul_your_api_key_here` with your Ultralytics API key, then restart Cursor.
157
+
158
+ #### Or install manually:
159
+
160
+ Go to `Cursor Settings` -> `MCP` -> `Add new MCP Server` (or edit `~/.cursor/mcp.json` directly) and use the standard config above: `command` set to `npx`, `args` set to `["-y", "ultralytics-mcp@latest"]`, and `ULTRALYTICS_API_KEY` in `env`.
161
+
162
+ </details>
163
+
164
+ <details>
165
+ <summary>Gemini CLI</summary>
166
+
167
+ Follow the MCP install [guide](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md#configure-the-mcp-server-in-settingsjson), use the standard config above.
168
+
169
+ </details>
170
+
171
+ <details>
172
+ <summary>VS Code / Copilot</summary>
173
+
174
+ #### Click the button to install:
175
+
176
+ [<img src="https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20Server&color=0098FF" alt="Install in VS Code">](https://insiders.vscode.dev/redirect?url=vscode%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522ultralytics%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522-y%2522%252C%2522ultralytics-mcp%2540latest%2522%255D%252C%2522env%2522%253A%257B%2522ULTRALYTICS_API_KEY%2522%253A%2522ul_your_api_key_here%2522%257D%257D)
177
+
178
+ > **Important**
179
+ > The install button writes a placeholder key. After installing, open your VS Code MCP config and replace `ul_your_api_key_here` with your Ultralytics API key, then restart VS Code.
180
+
181
+ #### Or install manually:
182
+
183
+ Follow the MCP install [guide](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server), use the standard config above. You can also install the server using the VS Code CLI:
184
+
185
+ ```bash
186
+ # For VS Code
187
+ code --add-mcp '{"name":"ultralytics","command":"npx","args":["-y","ultralytics-mcp@latest"],"env":{"ULTRALYTICS_API_KEY":"ul_your_api_key_here"}}'
188
+ ```
189
+
190
+ After installation, the Ultralytics MCP server will be available for use with your GitHub Copilot agent in VS Code.
191
+
192
+ </details>
193
+
194
+ These examples track latest published npm release. Restart MCP client or session after upgrading so new server process picks up latest package.
195
+
116
196
  ## Verify Setup
117
197
 
118
198
  ### Claude Code
@@ -161,6 +241,8 @@ See [TOOLS.md](./TOOLS.md) for full parameter reference, safety notes, local-pat
161
241
  - `training_start` requires `confirm_cost: true`
162
242
  - Ambiguous project or dataset refs fail instead of guessing
163
243
  - Signed upload and download URLs do not forward `Authorization`
244
+ - Local upload tools read files from the MCP client host; approve calls only for paths you expect to share with Ultralytics
245
+ - `model_download` writes to the requested local path; review `output_path` and `overwrite` before approving
164
246
 
165
247
  ## Troubleshooting
166
248
 
@@ -185,7 +267,7 @@ characters after prefix.
185
267
  ### Manual server smoke test
186
268
 
187
269
  ```bash
188
- ULTRALYTICS_API_KEY=ul_your_api_key_here npx -y ultralytics-mcp
270
+ ULTRALYTICS_API_KEY=ul_your_api_key_here npx -y ultralytics-mcp@latest
189
271
  ```
190
272
 
191
273
  If command exits immediately with config error, fix environment first.
package/TOOLS.md CHANGED
@@ -16,6 +16,8 @@ Auto-generated reference for Ultralytics Platform MCP tools.
16
16
  - `dataset_upload_folder` uploads a local image folder.
17
17
  - `dataset_upload_video` extracts frames from a local video file with `ffmpeg`.
18
18
  - `model_download` writes model weights to a local destination path.
19
+ - Review local upload paths before approving tool calls; upload tools read from the MCP client host.
20
+ - Review `model_download.output_path` and `overwrite` before approving downloads.
19
21
 
20
22
  ## Cost and Safety
21
23
 
@@ -1,9 +1,9 @@
1
1
  /** Model weight download tool. Writes to an explicit local path; the signed-URL
2
2
  * fetch never forwards API credentials (handled by client.downloadBytes).
3
3
  */
4
- import { stat, writeFile } from "node:fs/promises";
4
+ import { lstat, rename, rm, stat, writeFile } from "node:fs/promises";
5
5
  import { homedir } from "node:os";
6
- import { dirname, join, resolve } from "node:path";
6
+ import { basename, dirname, join, resolve } from "node:path";
7
7
  import { resolveModel } from "../resolve.js";
8
8
  import { asRecord } from "./shared.js";
9
9
  function fileName(info) {
@@ -59,6 +59,31 @@ async function statSafe(target) {
59
59
  return { exists: false, isDir: false };
60
60
  }
61
61
  }
62
+ async function lstatSafe(target) {
63
+ try {
64
+ const info = await lstat(target);
65
+ return {
66
+ exists: true,
67
+ isDir: info.isDirectory(),
68
+ isSymlink: info.isSymbolicLink(),
69
+ };
70
+ }
71
+ catch {
72
+ return { exists: false, isDir: false, isSymlink: false };
73
+ }
74
+ }
75
+ async function writeFileAtomic(target, content) {
76
+ const parent = dirname(target);
77
+ const temporary = join(parent, `.${basename(target)}.${process.pid}.${Date.now()}.tmp`);
78
+ try {
79
+ await writeFile(temporary, content, { flag: "wx" });
80
+ await rename(temporary, target);
81
+ }
82
+ catch (error) {
83
+ await rm(temporary, { force: true }).catch(() => undefined);
84
+ throw error;
85
+ }
86
+ }
62
87
  async function downloadTarget(outputPath, overwrite) {
63
88
  if (!outputPath?.trim()) {
64
89
  throw new Error("`output_path` is required.");
@@ -72,10 +97,13 @@ async function downloadTarget(outputPath, overwrite) {
72
97
  if (!parentInfo.isDir) {
73
98
  throw new Error(`Output parent is not a directory: ${parent}`);
74
99
  }
75
- const targetInfo = await statSafe(target);
100
+ const targetInfo = await lstatSafe(target);
76
101
  if (targetInfo.exists && targetInfo.isDir) {
77
102
  throw new Error(`Output path is a directory: ${target}`);
78
103
  }
104
+ if (targetInfo.exists && targetInfo.isSymlink) {
105
+ throw new Error(`Output path is a symbolic link: ${target}`);
106
+ }
79
107
  if (targetInfo.exists && !overwrite) {
80
108
  throw new Error(`Output path exists: ${target}. Pass overwrite=true to replace it.`);
81
109
  }
@@ -94,7 +122,7 @@ export async function modelDownload(client, model, options) {
94
122
  throw new Error(`Model file '${selectedName}' did not include a download URL.`);
95
123
  }
96
124
  const content = await client.downloadBytes(signedUrl);
97
- await writeFile(target, content);
125
+ await writeFileAtomic(target, content);
98
126
  return {
99
127
  summary: `Downloaded ${selectedName} to ${target} (${content.length} bytes).`,
100
128
  data: {
@@ -1,9 +1,9 @@
1
1
  /** Tool registration for the MCP server.
2
2
  *
3
3
  * Logic functions live in the sibling modules and are re-exported for tests and
4
- * the parity fixture runner. `registerReadTools` wires them onto an `McpServer`
5
- * with Zod input schemas. User-facing tool names stay snake_case for parity with
6
- * the Python package.
4
+ * the parity fixture runner. Registration helpers wire tools onto an
5
+ * `McpServer` with Zod input schemas. User-facing tool names stay snake_case for
6
+ * parity with the Python package.
7
7
  */
8
8
  import { z } from "zod";
9
9
  import { toMcpTextResult } from "../tool-result.js";
@@ -72,7 +72,7 @@ export const TOOL_DEFINITIONS = [
72
72
  }),
73
73
  tool({
74
74
  name: "projects_create",
75
- registrationGroup: "read",
75
+ registrationGroup: "write",
76
76
  stateChanging: true,
77
77
  description: "Create a project in your Ultralytics workspace.",
78
78
  inputSchema: {
@@ -93,7 +93,7 @@ export const TOOL_DEFINITIONS = [
93
93
  }),
94
94
  tool({
95
95
  name: "projects_delete",
96
- registrationGroup: "read",
96
+ registrationGroup: "write",
97
97
  stateChanging: true,
98
98
  description: "Soft-delete a project by id, slug, username/slug, or project ul:// URI.",
99
99
  inputSchema: {
@@ -155,7 +155,7 @@ export const TOOL_DEFINITIONS = [
155
155
  }),
156
156
  tool({
157
157
  name: "datasets_create",
158
- registrationGroup: "read",
158
+ registrationGroup: "write",
159
159
  stateChanging: true,
160
160
  description: "Create a dataset in your Ultralytics workspace.",
161
161
  inputSchema: {
@@ -230,7 +230,7 @@ export const TOOL_DEFINITIONS = [
230
230
  }),
231
231
  tool({
232
232
  name: "dataset_version_create",
233
- registrationGroup: "read",
233
+ registrationGroup: "write",
234
234
  stateChanging: true,
235
235
  description: "Create a frozen dataset version snapshot.",
236
236
  inputSchema: {
@@ -251,7 +251,7 @@ export const TOOL_DEFINITIONS = [
251
251
  }),
252
252
  tool({
253
253
  name: "datasets_delete",
254
- registrationGroup: "read",
254
+ registrationGroup: "write",
255
255
  stateChanging: true,
256
256
  description: "Soft-delete a dataset by id, slug, username/slug, or dataset ul:// URI.",
257
257
  inputSchema: {
@@ -268,7 +268,7 @@ export const TOOL_DEFINITIONS = [
268
268
  }),
269
269
  tool({
270
270
  name: "dataset_ingest",
271
- registrationGroup: "read",
271
+ registrationGroup: "write",
272
272
  stateChanging: true,
273
273
  description: "Start a remote URL ingest job for an existing dataset.",
274
274
  inputSchema: {
@@ -292,7 +292,7 @@ export const TOOL_DEFINITIONS = [
292
292
  }),
293
293
  tool({
294
294
  name: "dataset_upload_file",
295
- registrationGroup: "read",
295
+ registrationGroup: "write",
296
296
  stateChanging: true,
297
297
  description: "Upload a local dataset archive file and start ingest for an existing dataset.",
298
298
  inputSchema: {
@@ -326,7 +326,7 @@ export const TOOL_DEFINITIONS = [
326
326
  }),
327
327
  tool({
328
328
  name: "dataset_upload_folder",
329
- registrationGroup: "read",
329
+ registrationGroup: "write",
330
330
  stateChanging: true,
331
331
  description: "Upload a local image folder as a zip and start ingest for an existing dataset.",
332
332
  inputSchema: {
@@ -360,7 +360,7 @@ export const TOOL_DEFINITIONS = [
360
360
  }),
361
361
  tool({
362
362
  name: "dataset_upload_video",
363
- registrationGroup: "read",
363
+ registrationGroup: "write",
364
364
  stateChanging: true,
365
365
  description: "Upload a local video by extracting JPEG frames with ffmpeg, then start dataset ingest for an existing dataset.",
366
366
  inputSchema: {
@@ -443,7 +443,7 @@ export const TOOL_DEFINITIONS = [
443
443
  }),
444
444
  tool({
445
445
  name: "training_monitor",
446
- registrationGroup: "action",
446
+ registrationGroup: "read",
447
447
  stateChanging: false,
448
448
  description: "Report a model's training status and progress (works for private and public projects).",
449
449
  inputSchema: {
@@ -468,7 +468,7 @@ export const TOOL_DEFINITIONS = [
468
468
  }),
469
469
  tool({
470
470
  name: "model_predict",
471
- registrationGroup: "action",
471
+ registrationGroup: "read",
472
472
  stateChanging: false,
473
473
  description: "Run inference with a trained model on an image URL or base64 source (no local file paths).",
474
474
  inputSchema: {
@@ -554,7 +554,7 @@ export const TOOL_DEFINITIONS = [
554
554
  }),
555
555
  tool({
556
556
  name: "exports_list",
557
- registrationGroup: "write",
557
+ registrationGroup: "read",
558
558
  stateChanging: false,
559
559
  description: "List export jobs for a model.",
560
560
  inputSchema: {
@@ -568,7 +568,7 @@ export const TOOL_DEFINITIONS = [
568
568
  }),
569
569
  tool({
570
570
  name: "export_status",
571
- registrationGroup: "write",
571
+ registrationGroup: "read",
572
572
  stateChanging: false,
573
573
  description: "Get status for one export job by 24-character export id.",
574
574
  inputSchema: {
@@ -720,11 +720,11 @@ function registerToolDefinitions(server, getClient, registrationGroup) {
720
720
  export function registerReadTools(server, getClient) {
721
721
  registerToolDefinitions(server, getClient, "read");
722
722
  }
723
- /** Register training monitor, predict, and download tools. */
723
+ /** Register local action tools. */
724
724
  export function registerActionTools(server, getClient) {
725
725
  registerToolDefinitions(server, getClient, "action");
726
726
  }
727
- /** Register export and training-start tools. The cost-incurring ones are guarded. */
727
+ /** Register remote mutation tools. The cost-incurring ones are guarded. */
728
728
  export function registerWriteTools(server, getClient) {
729
729
  registerToolDefinitions(server, getClient, "write");
730
730
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultralytics-mcp",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "MCP for Ultralytics Platform workflows, datasets, training, prediction, and model operations.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,10 +37,16 @@
37
37
  "test": "vitest run"
38
38
  },
39
39
  "keywords": [
40
- "mcp",
41
40
  "ultralytics",
42
41
  "yolo",
43
- "model-context-protocol"
42
+ "mcp",
43
+ "model-context-protocol",
44
+ "claude",
45
+ "codex",
46
+ "computer-vision",
47
+ "object-detection",
48
+ "model-training",
49
+ "inference"
44
50
  ],
45
51
  "author": "Aman Harsh",
46
52
  "license": "MIT",