ultralytics-mcp 0.1.1 → 0.1.2

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
@@ -8,13 +8,13 @@ Current milestone: read, monitor, predict, export, and initial project and
8
8
  dataset lifecycle tools are available. Additional resource-management tools
9
9
  land incrementally from here.
10
10
 
11
- ## Tools (18)
11
+ ## Tools (20)
12
12
 
13
13
  | Tool | Description |
14
14
  | --- | --- |
15
15
  | `projects_list` / `projects_get` | Browse projects |
16
16
  | `projects_create` / `projects_delete` | Create / soft-delete projects |
17
- | `datasets_list` / `datasets_get` / `datasets_create` / `datasets_delete` | Browse / create / soft-delete datasets |
17
+ | `datasets_list` / `datasets_get` / `datasets_create` / `datasets_delete` / `dataset_ingest` / `dataset_upload_file` | Browse / create / soft-delete datasets, start remote ingest jobs, and upload archive files |
18
18
  | `models_list` / `models_get` | Browse trained models and metrics |
19
19
  | `training_monitor` | Status, progress, and latest metrics |
20
20
  | `model_predict` | Run inference on an image URL or base64 source |
package/dist/client.js CHANGED
@@ -20,6 +20,7 @@ export class UltralyticsClient {
20
20
  maxRetries;
21
21
  fetchImpl;
22
22
  downloadFetchImpl;
23
+ uploadFetchImpl;
23
24
  constructor(options = {}) {
24
25
  this.baseUrl = (options.baseUrl ?? getApiBase()).replace(/\/+$/, "");
25
26
  this.apiKey = options.apiKey ?? getApiKey();
@@ -27,6 +28,7 @@ export class UltralyticsClient {
27
28
  this.maxRetries = options.maxRetries ?? 3;
28
29
  this.fetchImpl = options.fetchImpl ?? fetch;
29
30
  this.downloadFetchImpl = options.downloadFetchImpl ?? fetch;
31
+ this.uploadFetchImpl = options.uploadFetchImpl ?? fetch;
30
32
  }
31
33
  // -- public verbs --------------------------------------------------------
32
34
  /** GET requests are idempotent and retry 429 responses. */
@@ -87,6 +89,23 @@ export class UltralyticsClient {
87
89
  throw new Error("unreachable");
88
90
  }
89
91
  }
92
+ /** Upload bytes to a signed URL WITHOUT forwarding API credentials. */
93
+ async uploadBytes(url, content, contentType) {
94
+ const bytes = new Uint8Array(content.byteLength);
95
+ bytes.set(content);
96
+ const response = await this.fetchWithTimeout(this.uploadFetchImpl, url, {
97
+ method: "PUT",
98
+ headers: {
99
+ Accept: "*/*",
100
+ "Content-Type": contentType,
101
+ },
102
+ body: bytes,
103
+ });
104
+ if (response.ok) {
105
+ return;
106
+ }
107
+ await this.handle(response, url);
108
+ }
90
109
  // -- internals -----------------------------------------------------------
91
110
  buildUrl(path, params) {
92
111
  const suffix = path.startsWith("/") ? path : `/${path}`;
@@ -1,4 +1,6 @@
1
1
  /** Read-only dataset tools. */
2
+ import { readFile, stat } from "node:fs/promises";
3
+ import { basename } from "node:path";
2
4
  import { resolveDataset } from "../resolve.js";
3
5
  import { asRecord, listField, pyCount, pyField } from "./shared.js";
4
6
  const DATASET_TASKS = new Set([
@@ -9,10 +11,51 @@ const DATASET_TASKS = new Set([
9
11
  "pose",
10
12
  "obb",
11
13
  ]);
14
+ const TARGET_SPLITS = new Set(["train", "val", "test"]);
15
+ const MAX_UPLOAD_BYTES = 10 * 1024 * 1024 * 1024;
16
+ const UPLOAD_TYPES = [
17
+ [".tar.gz", "application/gzip"],
18
+ [".zip", "application/zip"],
19
+ [".tar", "application/x-tar"],
20
+ [".tgz", "application/gzip"],
21
+ [".ndjson", "application/x-ndjson"],
22
+ ];
12
23
  function resourceId(item, fallback) {
13
24
  const value = item._id ?? item.id ?? item.projectId ?? item.datasetId;
14
25
  return String(value ?? fallback ?? "None");
15
26
  }
27
+ function validateTargetSplit(targetSplit) {
28
+ if (targetSplit !== undefined && !TARGET_SPLITS.has(targetSplit)) {
29
+ const allowed = Array.from(TARGET_SPLITS).sort().join(", ");
30
+ throw new Error(`Unsupported targetSplit '${targetSplit}'. Expected one of: ${allowed}.`);
31
+ }
32
+ }
33
+ async function datasetUploadFileMeta(filePath) {
34
+ if (!filePath.trim()) {
35
+ throw new Error("`filePath` is required.");
36
+ }
37
+ const info = await stat(filePath).catch(() => null);
38
+ if (info === null) {
39
+ throw new Error(`Upload file does not exist: ${filePath}`);
40
+ }
41
+ if (!info.isFile()) {
42
+ throw new Error(`Upload path is not a file: ${filePath}`);
43
+ }
44
+ if (info.size >= MAX_UPLOAD_BYTES) {
45
+ throw new Error("Upload file must be smaller than 10 GB.");
46
+ }
47
+ const filename = basename(filePath);
48
+ const lower = filename.toLowerCase();
49
+ const matched = UPLOAD_TYPES.find(([suffix]) => lower.endsWith(suffix));
50
+ if (!matched) {
51
+ throw new Error("Unsupported dataset upload file type. Expected one of: .zip, .tar, .tar.gz, .tgz, .ndjson.");
52
+ }
53
+ return {
54
+ filename,
55
+ contentType: matched[1],
56
+ totalBytes: info.size,
57
+ };
58
+ }
16
59
  /** List datasets in the workspace, optionally filtered by username. */
17
60
  export async function datasetsList(client, username) {
18
61
  const data = await client.get("/datasets", username ? { username } : undefined);
@@ -83,3 +126,59 @@ export async function datasetsDelete(client, dataset) {
83
126
  data: { id: datasetId, response: data },
84
127
  };
85
128
  }
129
+ /** Start a remote URL ingest job for an existing dataset. */
130
+ export async function datasetsIngest(client, options) {
131
+ if (!options.sourceUrl.trim()) {
132
+ throw new Error("`sourceUrl` is required.");
133
+ }
134
+ validateTargetSplit(options.targetSplit);
135
+ const datasetId = await resolveDataset(client, options.dataset);
136
+ const payload = {
137
+ datasetId,
138
+ sourceUrl: options.sourceUrl,
139
+ };
140
+ if (options.targetSplit !== undefined) {
141
+ payload.targetSplit = options.targetSplit;
142
+ }
143
+ const data = await client.postJson("/datasets/ingest", payload);
144
+ const item = asRecord(data);
145
+ const jobId = item.jobId ?? item.id ?? "None";
146
+ return {
147
+ summary: `Started dataset ingest job ${String(jobId)} for dataset ${datasetId}.`,
148
+ data: item,
149
+ };
150
+ }
151
+ /** Upload a local dataset archive file, then start ingest for that upload. */
152
+ export async function datasetUploadFile(client, options) {
153
+ validateTargetSplit(options.targetSplit);
154
+ const meta = await datasetUploadFileMeta(options.filePath);
155
+ const datasetId = await resolveDataset(client, options.dataset);
156
+ const content = await readFile(options.filePath);
157
+ const signed = asRecord(await client.postJson("/upload/signed-url", {
158
+ assetType: "datasets",
159
+ assetId: datasetId,
160
+ filename: meta.filename,
161
+ contentType: meta.contentType,
162
+ totalBytes: meta.totalBytes,
163
+ }));
164
+ const uploadUrl = String(signed.url ?? "");
165
+ const sessionId = String(signed.sessionId ?? "");
166
+ await client.uploadBytes(uploadUrl, content, meta.contentType);
167
+ await client.postJson("/upload/complete", { sessionId });
168
+ const ingestPayload = { datasetId, sessionId };
169
+ if (options.targetSplit !== undefined) {
170
+ ingestPayload.targetSplit = options.targetSplit;
171
+ }
172
+ const ingest = asRecord(await client.postJson("/datasets/ingest", ingestPayload));
173
+ const jobId = ingest.jobId ?? ingest.id ?? "None";
174
+ return {
175
+ summary: `Uploaded ${meta.filename} (${meta.totalBytes} bytes) and started dataset ingest job ${String(jobId)}.`,
176
+ data: {
177
+ datasetId,
178
+ filename: meta.filename,
179
+ bytes: meta.totalBytes,
180
+ sessionId,
181
+ ingest,
182
+ },
183
+ };
184
+ }
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { z } from "zod";
9
9
  import { toMcpTextResult } from "../tool-result.js";
10
- import { datasetsCreate, datasetsDelete, datasetsGet, datasetsList, } from "./datasets.js";
10
+ import { datasetsCreate, datasetsDelete, datasetsGet, datasetsIngest, datasetsList, datasetUploadFile, } from "./datasets.js";
11
11
  import { modelDownload } from "./downloads.js";
12
12
  import { exportCreate, exportStatus, exportsList } from "./exports.js";
13
13
  import { gpuAvailability } from "./gpu.js";
@@ -15,7 +15,7 @@ import { modelsGet, modelsList } from "./models.js";
15
15
  import { modelPredict } from "./predict.js";
16
16
  import { projectsCreate, projectsDelete, projectsGet, projectsList, } from "./projects.js";
17
17
  import { trainingMonitor, trainingStart } from "./training.js";
18
- export { datasetsCreate, datasetsDelete, datasetsGet, datasetsList, } from "./datasets.js";
18
+ export { datasetsCreate, datasetsDelete, datasetsGet, datasetsIngest, datasetsList, datasetUploadFile, } from "./datasets.js";
19
19
  export { modelDownload } from "./downloads.js";
20
20
  export { exportCreate, exportStatus, exportsList } from "./exports.js";
21
21
  export { gpuAvailability } from "./gpu.js";
@@ -33,6 +33,8 @@ export const READ_TOOL_NAMES = [
33
33
  "datasets_get",
34
34
  "datasets_create",
35
35
  "datasets_delete",
36
+ "dataset_ingest",
37
+ "dataset_upload_file",
36
38
  "models_list",
37
39
  "models_get",
38
40
  "gpu_availability",
@@ -89,6 +91,26 @@ export function registerReadTools(server, getClient) {
89
91
  description: "Soft-delete a dataset by id, slug, username/slug, or dataset ul:// URI.",
90
92
  inputSchema: { dataset: z.string() },
91
93
  }, async ({ dataset }) => toMcpTextResult(await datasetsDelete(getClient(), dataset)));
94
+ server.registerTool("dataset_ingest", {
95
+ description: "Start a remote URL ingest job for an existing dataset.",
96
+ inputSchema: {
97
+ dataset: z.string(),
98
+ sourceUrl: z.string(),
99
+ targetSplit: z.string().optional(),
100
+ },
101
+ }, async ({ dataset, sourceUrl, targetSplit }) => toMcpTextResult(await datasetsIngest(getClient(), { dataset, sourceUrl, targetSplit })));
102
+ server.registerTool("dataset_upload_file", {
103
+ description: "Upload a local dataset archive file and start ingest for an existing dataset.",
104
+ inputSchema: {
105
+ dataset: z.string(),
106
+ file_path: z.string(),
107
+ targetSplit: z.string().optional(),
108
+ },
109
+ }, async ({ dataset, file_path, targetSplit }) => toMcpTextResult(await datasetUploadFile(getClient(), {
110
+ dataset,
111
+ filePath: file_path,
112
+ targetSplit,
113
+ })));
92
114
  server.registerTool("models_list", {
93
115
  description: "List models in a project by project id, slug, username/slug, or project ul:// URI.",
94
116
  inputSchema: { project: z.string() },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ultralytics-mcp",
3
- "version": "0.1.1",
4
- "description": "TypeScript MCP server for the Ultralytics Platform REST API.",
3
+ "version": "0.1.2",
4
+ "description": "MCP for Ultralytics Platform workflows, datasets, training, prediction, and model operations.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "ultralytics-mcp": "./dist/cli.js"