Skip to content
Open
Show file tree
Hide file tree
Changes from 94 commits
Commits
Show all changes
121 commits
Select commit Hold shift + click to select a range
6ba95b7
feat: Add get-actor-version tool in a new source category
DaveHanns Sep 25, 2026
f45d08d
fix: Harden get-actor-version zip reading, inline files, and hints
DaveHanns Sep 25, 2026
6851981
docs: Link the current API pages in get-actor-version
DaveHanns Sep 25, 2026
a27395e
feat: Add create-actor and update-actor-version tools
DaveHanns Sep 25, 2026
534127d
fix: Harden create-actor and update-actor-version writes
DaveHanns Sep 25, 2026
f398803
refactor: Share file input parsing between source write tools
DaveHanns Sep 25, 2026
83951fd
docs: Mark where get-actor-version reads a whole source
DaveHanns Sep 25, 2026
8f297a1
Merge remote-tracking branch 'origin/feat/get-actor-version' into fea…
DaveHanns Sep 25, 2026
e2d7e8a
docs: Mark where update-actor-version writes a whole source
DaveHanns Sep 25, 2026
b9df355
refactor: Refuse zip-stored versions in get-actor-version and simplif…
DaveHanns Sep 27, 2026
24f9792
Merge branch 'feat/get-actor-version' into feat/update-actor-version
DaveHanns Sep 27, 2026
aa2d8fe
refactor: Share the manifest builder and align the zip refusal
DaveHanns Sep 27, 2026
8f2f928
refactor: Tidy get-actor-version after dropping zip reading
DaveHanns Sep 28, 2026
fa1c1b9
Merge branch 'feat/get-actor-version' into feat/update-actor-version
DaveHanns Sep 28, 2026
2f77449
refactor: Decode stored source entries in one place
DaveHanns Sep 28, 2026
4daa78b
refactor: Simplify get-actor-version
DaveHanns Sep 29, 2026
1b28077
refactor: Address review of the get-actor-version simplification
DaveHanns Sep 29, 2026
46a53c3
Merge branch 'feat/get-actor-version' into feat/update-actor-version
DaveHanns Sep 29, 2026
1c7bcab
refactor: Keep the write tools' source helpers after the get-actor-ve…
DaveHanns Sep 29, 2026
0519a95
refactor: Simplify create-actor and update-actor-version
DaveHanns Sep 29, 2026
639cffc
refactor: Address review of the create-actor and update-actor-version…
DaveHanns Sep 29, 2026
e489d3c
Merge remote-tracking branch 'origin/master' into feat/get-actor-version
DaveHanns Sep 29, 2026
0ecc220
Merge branch 'feat/get-actor-version' into feat/update-actor-version
DaveHanns Sep 29, 2026
b9e02ca
refactor: Report only API errors from the build start as buildError
DaveHanns Sep 29, 2026
f3da605
Merge remote-tracking branch 'origin/master' into feat/get-actor-version
DaveHanns Sep 30, 2026
5650f16
Merge branch 'feat/get-actor-version' into feat/update-actor-version
DaveHanns Sep 30, 2026
48f8db4
test: Cover read-only calls, env var leaks, and sub-resources in get-…
DaveHanns Sep 30, 2026
749d1f3
test: Cover exact content, line range edges, and the byte limit in ge…
DaveHanns Sep 30, 2026
8b22d9d
test: Cover hashes, revisions, path order, and URL redaction in get-a…
DaveHanns Sep 30, 2026
20b4796
test: Cover the text block, edge-case files, and the 256 KiB limit in…
DaveHanns Sep 30, 2026
c47b869
Merge branch 'feat/get-actor-version' into feat/update-actor-version
DaveHanns Sep 30, 2026
86fac27
test: Expect the shared refusal wording in the new get-actor-version …
DaveHanns Sep 30, 2026
11a6247
test: Cover untouched files, the PUT body, operation order, and refus…
DaveHanns Sep 30, 2026
44a3b59
test: Cover the POST body, file mapping, cancellation, and autoBuild …
DaveHanns Sep 30, 2026
9a88b74
test: Cover empty files in the PUT, multi-edit, whole-value hash chec…
DaveHanns Sep 30, 2026
49497d5
test: Cover a cancel during the build start in update-actor-version
DaveHanns Sep 30, 2026
704f329
refactor: Address review nits in get-actor-version
DaveHanns Oct 1, 2026
41285bc
fix: Name left-out and missing files in the get-actor-version summary
DaveHanns Oct 1, 2026
bae722b
fix: Normalize requested paths in get-actor-version
DaveHanns Oct 1, 2026
1b372e9
fix: Explain in plain words why get-actor-version refuses a version
DaveHanns Oct 1, 2026
0a183a5
test: Merge near-duplicate get-actor-version tests into it.each
DaveHanns Oct 1, 2026
5f6f54e
Merge branch 'feat/get-actor-version' into feat/update-actor-version
DaveHanns Oct 1, 2026
95785dd
refactor: Read the eager file content in update-actor-version
DaveHanns Oct 1, 2026
ea9a5e7
test: Expect the new refusal wording in update-actor-version
DaveHanns Oct 1, 2026
b25a51a
style: Format the refusal tests in update-actor-version
DaveHanns Oct 1, 2026
ea000a6
Merge remote-tracking branch 'origin/master' into feat/update-actor-v…
DaveHanns Oct 5, 2026
1ac3793
test: Cover stored names, the input schema, and more cases of update-…
DaveHanns Oct 5, 2026
379e254
test: Cover the input schema, Console link, and non-ASCII paths of cr…
DaveHanns Oct 5, 2026
72413ad
test: Check that get-actor-version names the Actor by its document
DaveHanns Oct 5, 2026
824b2a4
fix: Count overlapping matches of an oldText as several
DaveHanns Oct 5, 2026
5e479d1
fix: Refuse to write a file outside the Actor root or at a folder path
DaveHanns Oct 5, 2026
4c310fa
fix: Refuse a new file whose path collides with a file or folder
DaveHanns Oct 5, 2026
22d7709
fix: Refuse two files at the same path in create-actor
DaveHanns Oct 5, 2026
43823d7
fix: Check the expectedHash an edit operation carries
DaveHanns Oct 5, 2026
2ff9fb1
fix: Refuse content the stored file would not hold as sent
DaveHanns Oct 5, 2026
f82051c
fix: Report any failed build start after a write as buildError
DaveHanns Oct 5, 2026
94c660a
fix: Keep line endings CRLF when an LF edit changes a CRLF file
DaveHanns Oct 5, 2026
cfd8a4d
fix: Say that an Actor has no versions instead of listing none
DaveHanns Oct 5, 2026
32eed8c
test: Run create-actor, get-actor-version, and update-actor-version a…
DaveHanns Oct 5, 2026
dec4503
test: Add live integration cases for create-actor and update-actor-ve…
DaveHanns Oct 5, 2026
f4768c3
test: Remove the second live case for the source tools from actors.ca…
DaveHanns Oct 5, 2026
6cf8057
test: Seed entries without format or content in the live update case
DaveHanns Oct 5, 2026
b8cdc79
test: Check that update-actor-version names no deleted empty file as …
DaveHanns Oct 5, 2026
2a51ce4
test: Check that a folder stored under an unnormalized name blocks a …
DaveHanns Oct 5, 2026
3668c69
fix: Say that a retried create-actor POST may have created the Actor
DaveHanns Oct 5, 2026
f06edcd
Revert "fix: Say that an Actor has no versions instead of listing none"
DaveHanns Oct 7, 2026
7387891
Merge remote-tracking branch 'origin/master' into feat/update-actor-v…
DaveHanns Oct 7, 2026
986367d
fix: Name get-actor-version in the retried create-actor message only …
DaveHanns Oct 7, 2026
8eee7c8
fix: Say to build the version before running it, not to run the files
DaveHanns Oct 7, 2026
9cc64d9
fix: Say create-actor uploads new Actor code, not publishes it
DaveHanns Oct 7, 2026
88a8173
docs: Make the Actor source feature line one plain list
DaveHanns Oct 7, 2026
2774c2c
Merge remote-tracking branch 'origin/master' into feat/update-actor-v…
DaveHanns Oct 12, 2026
90737fb
fix: Limit update-actor-version to 100 operations and 100 edits per call
DaveHanns Oct 12, 2026
cf835c2
fix: Keep file content out of the logged arguments of the source tools
DaveHanns Oct 12, 2026
163d817
fix: Refuse control characters in source paths and encode the revisio…
DaveHanns Oct 12, 2026
9930b52
fix: Start no build after a source write when the call is cancelled d…
DaveHanns Oct 12, 2026
a2ac5fd
fix: Search an oldText over 250 characters in time linear in the file
DaveHanns Oct 12, 2026
6817b57
fix: Check create-actor file paths in time linear in the file count
DaveHanns Oct 12, 2026
300b1d1
Merge remote-tracking branch 'origin/master' into feat/update-actor-v…
DaveHanns Oct 16, 2026
0afdf69
fix: Cap update-actor-version at 20 edits per call
DaveHanns Oct 16, 2026
eced09b
fix: State facts instead of next calls in source tool texts
DaveHanns Oct 16, 2026
43e34e4
fix: Mark create-actor and update-actor-version as open-world
DaveHanns Oct 16, 2026
03a361a
fix: Take the create-actor version from .actor/actor.json
DaveHanns Oct 16, 2026
873e03e
refactor: Start the post-write build with the client directly
DaveHanns Oct 16, 2026
09812ec
refactor: Drop the post-write cancel checks in source write tools
DaveHanns Oct 16, 2026
f7445a7
test: Bound the 20-edit timing test at 6 s and say what it guards
DaveHanns Oct 16, 2026
a3d330a
fix: Read an empty actor.json version as none in create-actor
DaveHanns Oct 16, 2026
3fd4b24
fix: State what the update-actor-version revision is for as a fact
DaveHanns Oct 16, 2026
cb07786
fix: Take a null expectedHash, expectedRevision, or versionNumber as …
DaveHanns Oct 16, 2026
5c1d8ca
fix: Refuse a source path with a lone UTF-16 surrogate
DaveHanns Oct 16, 2026
be2dc8c
fix: Resolve a bare Actor name in the source tools
DaveHanns Oct 16, 2026
fb1c7c9
test: Read the only version for a null versionNumber in get-actor-ver…
DaveHanns Oct 16, 2026
9dc70f8
Merge remote-tracking branch 'origin/master' into feat/update-actor-v…
DaveHanns Oct 16, 2026
90f3108
test: Expect the taken-name error without the input hint master dropped
DaveHanns Oct 16, 2026
0d5b6f6
refactor: Move the ID-or-name resolver out of the task helpers
DaveHanns Oct 16, 2026
29ed1be
refactor: Import the Actor resolver from the shared resource ID module
DaveHanns Oct 16, 2026
8092217
refactor!: Rename the source tool category to versions
DaveHanns Oct 16, 2026
978d3bf
refactor: Move the versions tools to a versions folder
DaveHanns Oct 16, 2026
abfcc2b
fix: Start no build when update-actor-version changes nothing
DaveHanns Oct 16, 2026
084952f
fix: Say only that no build was started when update-actor-version cha…
DaveHanns Oct 16, 2026
7b6d9af
fix: Resolve a bare Actor name in build-actor
DaveHanns Oct 16, 2026
cf4e7de
fix: Resolve a bare Actor name in the build and run list filters
DaveHanns Oct 16, 2026
29a14b9
fix: Refuse an update-actor-version edit that grows a file past 3 MiB
DaveHanns Oct 19, 2026
12c6a0b
fix: Read the version of an .actor/actor.json that starts with a byte…
DaveHanns Oct 19, 2026
81d0b5e
fix: Say a 5xx or unanswered version PUT may have saved the change
DaveHanns Oct 19, 2026
68f10af
test: Check that update-actor-version refuses a NUL path in a single …
DaveHanns Oct 19, 2026
0c7e960
test: Check the build hint of update-actor-version in a session witho…
DaveHanns Oct 19, 2026
ce891f4
test: Abort the build of the live create-actor case in finally
DaveHanns Oct 19, 2026
bc9e2c3
fix: Refuse a backslash in the path of a written file
DaveHanns Oct 19, 2026
4984455
fix: Report a sub-resource reached by extra path segments in build-ac…
DaveHanns Oct 19, 2026
49abeec
refactor: Extract redactEdits from redactSourceToolArgs
DaveHanns Oct 19, 2026
b2af38b
refactor: Split applyOperation of update-actor-version by operation type
DaveHanns Oct 19, 2026
e2527c0
refactor: Extract startBuildAfterWrite from respondAfterWrite
DaveHanns Oct 19, 2026
61a58c4
refactor: Start the create-actor catch with a guard for a retried nam…
DaveHanns Oct 19, 2026
0fc3c09
refactor: Check file and folder collisions with one path index in bot…
DaveHanns Oct 19, 2026
bd0daaf
fix: Say a version PUT that failed after a retry may have saved the c…
DaveHanns Oct 19, 2026
766caf8
fix: Say no build was started after an unconfirmed save with autoBuild
DaveHanns Oct 19, 2026
9ee727c
test: Check that update-actor-version refuses an oversized edit befor…
DaveHanns Oct 19, 2026
a2f8494
refactor: Write the byte order mark in create-actor as an escape
DaveHanns Oct 19, 2026
4f5ff55
fix: Count only the LFs of newText that become CRLF in the length che…
DaveHanns Oct 19, 2026
e24a7e1
test: Log a failed build abort of the live create-actor case instead …
DaveHanns Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ After `pnpm run build`, run `mcpc` (no args) to check sessions: if `@stdio` (def
- **Integration tests**: `pnpm run test:integration` (needs build + `APIFY_TOKEN`, humans only).
- **Conformance tests**: `pnpm run test:conformance` (needs `APIFY_TOKEN`) builds and runs both protocol eras locally. `_integration_tests.yaml` calls `_conformance_tests.yaml` for the same `2026-07-28` and `2025-11-25` coverage in CI. Both eras always run; the command exits with the first non-zero code. Scenarios excluded from each era's gate are listed with reasons in `scripts/conformance_expected_failures_2026_07_28.yaml` and `scripts/conformance_expected_failures_2025_11_25.yaml`.
- **Package manager**: this repo uses **pnpm 11+**. `devEngines.packageManager` is pinned with `onFail: "error"`, so npm / yarn refuse to run inside the checkout — use `pnpm install` only.
- `tests/integration/suite.ts` wires the `stdio`, `2025-11-25` (streamable HTTP) and `2026-07-28` (stateless HTTP) transport dimensions into one shared suite. Add new integration cases to the matching capability array in `tests/test_kit/cases/*.cases.ts` (registration, tools, actors, apps, tasks, storage, schedules, payments) — each is a plain `Case[]` (`{ name, isDeploymentTest, run, ... }`), registered via `registerCases(name, cases, ctx)`. Mark a case `isDeploymentTest: true` only if it's worth running against `apify-mcp-server-internal`'s live staging and production deploys — the flag doesn't just share the case, it makes internal execute it for real on every release (see [DEVELOPMENT.md](./DEVELOPMENT.md#test-organization-across-repos)). Shared assertion helpers live in `tests/test_kit/helpers.ts`. Cases that share expensive setup (e.g. one seeded Actor run, see `storage.cases.ts`'s `normalModeRunFixture`) do so via `ctx.getFixture(fixture)`, memoized once per transport dimension — not a vitest `beforeAll`, so any subset of cases can share it while staying flat `Case` objects.
- `tests/integration/suite.ts` wires the `stdio`, `2025-11-25` (streamable HTTP) and `2026-07-28` (stateless HTTP) transport dimensions into one shared suite. Add new integration cases to the matching capability array in `tests/test_kit/cases/*.cases.ts` (registration, tools, actors, apps, tasks, storage, schedules, source, payments) — each is a plain `Case[]` (`{ name, isDeploymentTest, run, ... }`), registered via `registerCases(name, cases, ctx)`. Mark a case `isDeploymentTest: true` only if it's worth running against `apify-mcp-server-internal`'s live staging and production deploys — the flag doesn't just share the case, it makes internal execute it for real on every release (see [DEVELOPMENT.md](./DEVELOPMENT.md#test-organization-across-repos)). Shared assertion helpers live in `tests/test_kit/helpers.ts`. Cases that share expensive setup (e.g. one seeded Actor run, see `storage.cases.ts`'s `normalModeRunFixture`) do so via `ctx.getFixture(fixture)`, memoized once per transport dimension — not a vitest `beforeAll`, so any subset of cases can share it while staying flat `Case` objects.
- Follow existing test patterns (names, structure) — check neighboring files.
- **Test naming**: `describe('fnName()')`, plain-verb `it()` names (no `should` prefix). Group with nested `describe()` per method when a factory/class exposes several.

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ Here are some special MCP operations and how the Apify MCP Server supports them:
- **Actor tasks**: Create, inspect, and update your saved Actor tasks, and publish or unpublish their public landing pages.
- **Schedules**: Create, inspect, update, and delete schedules that run your Actors and tasks automatically.
- **Builds**: Build an Actor version, list builds, check the status of a build, and retrieve its build log.
- **Actor source**: Read an Actor version's files, with a hash for each file and a revision for the whole set.
- **Actor source**: Read an Actor version's files with a hash for each file and a revision for the whole set, create an Actor from files, and change a version's files with checks that catch changes made since your last read.
- **Apify API**: Search the Apify API reference, inspect an operation, and send requests to the API to read or change data.

### Overview of available tools
Expand Down Expand Up @@ -299,6 +299,8 @@ Legend for the **Enabled by default** column:
| `get-actor-build-list` | builds | List the account's builds, or one Actor's, in every status, newest first. | |
| `build-actor` | builds | Build an Actor version and wait a bounded time for the build to finish. | |
| `get-actor-version` | source | Get an Actor version's file listing with hashes and the content of the files you ask for. | |
| `create-actor` | source | Create a private Actor in your account from files, optionally starting a build. | |
| `update-actor-version` | source | Write, edit, or delete files of an Actor version in one all-or-nothing write. | |
| `apify-api-search` | api | Search the Apify API reference for operations by keywords. Returns each one's method, path, and docs page. | |
| `apify-api-details` | api | Get the parameters and request body schema of the Apify API operations on a path. | |
| `apify-api-read` | api | Send a GET request to an Apify API path, such as `actor-runs` or `/v2/actor-runs`, like `apify api` in the Apify CLI. Read-only: it refuses the `method` query parameter and a GET to the synchronous run endpoints, which starts a paid run. | |
Expand Down
3 changes: 3 additions & 0 deletions src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const HELPER_TOOLS = {
ACTOR_BUILD_LOG: 'get-actor-build-log',
ACTOR_BUILD_LIST_GET: 'get-actor-build-list',
ACTOR_CALL: 'call-actor',
ACTOR_CREATE: 'create-actor',
ACTOR_CALL_WIDGET: 'call-actor-widget',
ACTOR_DELETE: 'delete-actor',
ACTOR_GET_DETAILS: 'fetch-actor-details',
Expand All @@ -70,6 +71,7 @@ export const HELPER_TOOLS = {
ACTOR_TASK_PUBLISH: 'publish-actor-task',
ACTOR_TASK_UNPUBLISH: 'unpublish-actor-task',
ACTOR_VERSION_GET: 'get-actor-version',
ACTOR_VERSION_UPDATE: 'update-actor-version',
DATASET_GET: 'get-dataset',
DATASET_LIST_GET: 'get-dataset-list',
DATASET_GET_ITEMS: 'get-dataset-items',
Expand Down Expand Up @@ -283,6 +285,7 @@ export const APIFY_ERROR_TYPE_MEMORY_LIMIT_EXCEEDED = 'memory-limit-exceeded';
export const APIFY_ERROR_TYPE_CANNOT_START_ACTOR_RUNS = 'cannot-start-actor-runs';
export const APIFY_ERROR_TYPE_CANNOT_PUBLISH_ACTOR_TASK = 'cannot-publish-actor-task';
export const APIFY_ERROR_TYPE_INVALID_INPUT = 'invalid-input';
export const APIFY_ERROR_TYPE_ACTOR_NAME_NOT_UNIQUE = 'actor-name-not-unique';
export const APIFY_ERROR_TYPE_PAGE_NOT_FOUND = 'page-not-found';

// HTTP status codes
Expand Down
8 changes: 6 additions & 2 deletions src/tools/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@ direct actor tools, `search-actors`, `fetch-actor-details`) is mode-agnostic.
allowlisted build result shape, the build start and wait calls (the wait reports progress), the
shared `waitSecs` field, the shared build response and the by-status next-step text.
- `source/`: `get-actor-version` (an Actor version's file manifest with hashes, revision, and
requested content). `source_files.ts` holds the file shape, the manifest builder, the hash and
revision rules, and the text-or-base64 detection. Versions not stored as files (Git repository,
requested content), `create-actor` (a new Actor with one version in a single POST), and
`update-actor-version` (write, edit, and delete operations applied all-or-nothing to a fresh read,
stored with one version PUT that carries only the source keys). `source_files.ts` holds the file
shape, the manifest builder, the hash and revision rules, and the text-or-base64 detection;
`source_helpers.ts` holds the Actor and version lookup, the file input shape, and the response
after a write, which starts the build for autoBuild. Versions not stored as files (Git repository,
gist, or zip) are refused.
- `api/` — the generic Apify API tools: search the operations of the published OpenAPI spec, get the
operations on a path, and send a GET, or a POST, PUT, PATCH, or DELETE, to a path. The write tool is in
Expand Down
4 changes: 3 additions & 1 deletion src/tools/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ import { createSchedule } from './schedules/create_schedule.js';
import { deleteSchedule } from './schedules/delete_schedule.js';
import { getSchedule } from './schedules/get_schedule.js';
import { updateSchedule } from './schedules/update_schedule.js';
import { createActor } from './source/create_actor.js';
import { getActorVersion } from './source/get_actor_version.js';
import { updateActorVersion } from './source/update_actor_version.js';
import { getDataset } from './storage/get_dataset.js';
import { getDatasetItems } from './storage/get_dataset_items.js';
import { getDatasetList } from './storage/get_dataset_list.js';
Expand Down Expand Up @@ -78,7 +80,7 @@ export const toolCategories = {
tasks: [createActorTask, getActorTask, updateActorTask, publishActorTask, unpublishActorTask],
schedules: [createSchedule, getSchedule, updateSchedule, deleteSchedule],
builds: [getActorBuild, getActorBuildLog, getActorBuildList, buildActor],
source: [getActorVersion],
source: [getActorVersion, createActor, updateActorVersion],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

source is a poor category name. It's a term we made up: neither the Apify API nor the Apify CLI uses it. The API calls this "Actor versions", and the CLI has no matching group (Actor code goes through actors push and actors pull).

I'd rename it to versions. That's not a great name either, because createActor isn't strictly a versions endpoint. But it matches the API vocabulary, and it stays consistent with runs, builds and tasks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. versions sounds better.

api: [apifyApiSearch, apifyApiDetails, apifyApiRead],
dev: [reportProblem],
} satisfies Record<string, ToolEntry[]>;
Expand Down
208 changes: 208 additions & 0 deletions src/tools/source/create_actor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import type { ActorCollectionCreateOptions, ActorVersionSourceFile } from 'apify-client';
import { ActorSourceType, ApifyApiError } from 'apify-client';
import dedent from 'dedent';
import { z } from 'zod';

import { APIFY_ERROR_TYPE_ACTOR_NAME_NOT_UNIQUE, HELPER_TOOLS } from '../../const.js';
import { UserInputError } from '../../errors.js';
import type { InternalToolArgs, ToolEntry, ToolInputSchema } from '../../types.js';
import { TOOL_TYPE } from '../../types.js';
import { compileSchema, fixZodSchemaRequired } from '../../utils/ajv.js';
import { respondAborted, respondUserError } from '../../utils/mcp.js';
import { createActorToolOutputSchema } from '../structured_output_schemas.js';
import { buildFilesManifest, buildFilesRevision } from './source_files.js';
import {
buildEmptyFilesWarnings,
buildSourceFileEntry,
extractSourceFiles,
redactSourceToolArgs,
resolveVersion,
respondAfterWrite,
respondToSourceToolError,
sourceFileArgs,
validateFileContent,
validateFilePath,
validatePathCharacters,
} from './source_helpers.js';

const createActorArgs = z.object({
name: z.string().min(1).describe('Name of the new Actor, without a username, for example my-scraper.'),
title: z.string().min(1).optional().describe('Display title, for example My Scraper.'),
description: z.string().optional().describe('A short description of what the Actor does.'),
files: z.array(sourceFileArgs).min(1).describe("The Actor's files, each with its whole content."),
versionNumber: z
.string()
.optional()
.describe(
'Number of the version in MAJOR.MINOR form. Default: the version in .actor/actor.json, or 0.0 when it has none.',
),
buildTag: z
.string()
.min(1)
.optional()
.describe('Tag that builds of the version get. The platform uses latest when it is omitted.'),
autoBuild: z
.boolean()
.default(false)
.describe('Start a build of the version after creating the Actor, and return without waiting. Default: false.'),
});

/** The folders above a file, outermost first: `a` and `a/b` for `a/b/c.js`. */
function listFolders(path: string): string[] {
const segments = path.split('/');
return segments.slice(1).map((_, index) => segments.slice(0, index + 1).join('/'));
}

/**
* The entries to send, one per file; throws `UserInputError` for a path no file can be written at, and for a path
* given twice, since the platform puts the Actor name in the first .actor/actor.json and the build uses the last.
* Paths are checked through a set and a map, since comparing each file with every earlier one took 8 s for 20,000.
*/
function buildSourceFileEntries(files: readonly z.infer<typeof sourceFileArgs>[]): ActorVersionSourceFile[] {
const entries: ActorVersionSourceFile[] = [];
const filePaths = new Set<string>();
const firstFileByFolder = new Map<string, string>();
for (const [index, file] of files.entries()) {
validatePathCharacters(file.path, `files[${index}]`);
const entry = buildSourceFileEntry(file);
const label = `files[${index}] (${file.path})`;
validateFilePath(entry.name, label);
validateFileContent(entry, label);
if (filePaths.has(entry.name)) {
throw new UserInputError(`${label} repeats the path ${entry.name}; each path can be sent only once.`);
}
const folders = listFolders(entry.name);
const collision = folders.find((folder) => filePaths.has(folder)) ?? firstFileByFolder.get(entry.name);
if (collision !== undefined) {
throw new UserInputError(
`${label} collides with ${collision}; one path cannot be both a file and a folder.`,
);
}
filePaths.add(entry.name);
for (const folder of folders) {
if (!firstFileByFolder.has(folder)) firstFileByFolder.set(folder, entry.name);
}
entries.push(entry);
}
return entries;
}

/** The `version` of .actor/actor.json when it is a non-empty string in valid JSON, as `apify push` reads it. */
function readActorJsonVersion(entries: readonly ActorVersionSourceFile[]): string | undefined {
const actorJson = entries.find(({ name, format }) => name === '.actor/actor.json' && format === 'TEXT');
if (!actorJson) return undefined;
try {
const { version } = JSON.parse(actorJson.content) as { version?: unknown };
return typeof version === 'string' && version !== '' ? version : undefined;
} catch {
return undefined;
}
}

/**
* https://docs.apify.com/api/v2/actors-post
* /v2/actors
*
* One POST creates the Actor and its version together, private and in the token's account. apify-client retries the
* POST after a network error, a timeout, a 429, or a 5xx, so when the platform saved an attempt it did not answer, the
* retry fails on the name that attempt took, and the call says the Actor may exist.
*/
export const createActor: ToolEntry = Object.freeze({
type: TOOL_TYPE.INTERNAL,
name: HELPER_TOOLS.ACTOR_CREATE,
title: 'Create Actor',
description: dedent`
Create a new private Actor in your account, with one version holding the files you give. It never changes an existing Actor: the platform refuses a name your account already uses.
- files: every file with its path and content. Binary files take base64 content with encoding base64; files with a binary extension such as .png default to it.
The platform sets the name field of .actor/actor.json to the Actor name, so the result lists each file's hash and the version's revision as stored.
autoBuild starts a build after creating the Actor and returns without waiting.

USAGE:
- Use to upload new Actor code to the Apify platform for the first time.

USAGE EXAMPLES:
- user_input: Create an Actor called hacker-news-scraper from these files`,
// `fixZodSchemaRequired` strips `autoBuild` from `required` because it has a default.
inputSchema: fixZodSchemaRequired(z.toJSONSchema(createActorArgs)) as ToolInputSchema,
outputSchema: createActorToolOutputSchema,
ajvValidate: compileSchema(z.toJSONSchema(createActorArgs)),
annotations: {
title: 'Create Actor',
readOnlyHint: false,
destructiveHint: false,
// A second call with the same name is refused rather than repeated.
idempotentHint: false,
// autoBuild starts a build, like build-actor.
openWorldHint: true,
},
redactArgs: (args: Record<string, unknown>) =>
redactSourceToolArgs(args, {
keys: ['name', 'title', 'description', 'versionNumber', 'buildTag', 'autoBuild'],
listKey: 'files',
entryKeys: ['path', 'encoding'],
}),
call: async (toolArgs: InternalToolArgs) => {
const { args, apifyClient: client, signal, loadedToolNames } = toolArgs;
const parsed = createActorArgs.parse(args);
// A cancel before the POST creates nothing; per the MCP spec the cancelled request gets no response.
if (signal?.aborted) return respondAborted();
try {
const sourceFiles = buildSourceFileEntries(parsed.files);
// The platform refuses a version without a number, so this defaults to what `apify push` uses.
// AJV turns a null versionNumber into an empty string, which counts as not given.
const versionNumber = parsed.versionNumber || readActorJsonVersion(sourceFiles) || '0.0';
const created = await client.actors().create({
name: parsed.name,
...(parsed.title !== undefined && { title: parsed.title }),
...(parsed.description !== undefined && { description: parsed.description }),
versions: [
{
versionNumber,
...(parsed.buildTag !== undefined && { buildTag: parsed.buildTag }),
sourceType: ActorSourceType.SourceFiles,
sourceFiles,
},
],
} satisfies ActorCollectionCreateOptions);
const fullName = `${created.username}/${created.name}`;
// The platform sets the name in .actor/actor.json on create, so the stored files, not the sent ones, match
// a later read.
const version = resolveVersion(created, undefined, fullName);
const files = buildFilesManifest(
extractSourceFiles(version, `Version ${version.versionNumber} of ${fullName}`),
);
const emptyPaths = files.filter(({ sizeBytes }) => sizeBytes === 0).map(({ path }) => path);
return await respondAfterWrite({
toolArgs,
autoBuild: parsed.autoBuild,
target: { actorId: created.id, versionNumber: version.versionNumber },
structuredContent: {
actorId: created.id,
fullName,
versionNumber: version.versionNumber,
revision: buildFilesRevision(files),
files: files.map(({ path, sizeBytes, hash }) => ({ path, sizeBytes, hash })),
...buildEmptyFilesWarnings(emptyPaths),
},
summary: `Created the private Actor ${fullName}.`,
});
} catch (error) {
if (
error instanceof ApifyApiError &&
error.type === APIFY_ERROR_TYPE_ACTOR_NAME_NOT_UNIQUE &&
error.attempt > 1
) {
const readTool = loadedToolNames.includes(HELPER_TOOLS.ACTOR_VERSION_GET)
? `; ${HELPER_TOOLS.ACTOR_VERSION_GET} reads its files`
: '';
return respondUserError(
`The platform refused the name ${parsed.name} as taken when the request was retried, so an earlier ` +
`attempt of this call may have created the Actor ${parsed.name}. ` +
`If it holds the files you sent, this call created it${readTool}.`,
{ httpStatus: error.statusCode },
);
}
return respondToSourceToolError(error);
}
},
} as const);
Loading
Loading