Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
145 commits
Select commit Hold shift + click to select a range
dec2ccd
feat: Add Apify API search, operation and read tools
DaveHanns Sep 25, 2026
76bc222
feat: Let an internal tool redact its own arguments in logs
DaveHanns Sep 23, 2026
be629e3
feat: Add write-apify-api to call POST and PUT API operations
DaveHanns Sep 25, 2026
2fad461
refactor: Rename the Apify API tools to the apify-api-* family
DaveHanns Sep 27, 2026
496bb6a
fix: Keep the token out of API tool errors and tighten the call checks
DaveHanns Sep 27, 2026
fce6462
Merge branch 'feat/apify-api-tools' into feat/apify-api-write
DaveHanns Sep 27, 2026
f8fcd56
refactor: Rename write-apify-api to apify-api-write
DaveHanns Sep 27, 2026
b697f5f
fix: Send apify-api-write bodies as JSON and keep secrets out of its …
DaveHanns Sep 27, 2026
25edeb1
feat: Refuse Actor source and version lists in apify-api-write
DaveHanns Sep 27, 2026
b2a4400
feat: Let an internal tool redact its own arguments in logs
DaveHanns Sep 23, 2026
1ef178a
fix: Redact API read arguments, skip error logs on cancel, and rank s…
DaveHanns Sep 27, 2026
5ad0f18
Merge branch 'feat/apify-api-tools' into feat/apify-api-write
DaveHanns Sep 27, 2026
66da602
fix: Make actor_versions_post unavailable and list every refused body…
DaveHanns Sep 27, 2026
362ba9a
feat: Identify Apify API operations by path instead of operation ID
DaveHanns Sep 28, 2026
cc4dced
Merge branch 'feat/apify-api-tools' into feat/apify-api-write
DaveHanns Sep 28, 2026
223ba45
feat: Call apify-api-write by path, with an optional method
DaveHanns Sep 28, 2026
481da45
fix: Redact a query in the logged path and echo templates as written
DaveHanns Sep 28, 2026
8e72c9a
Merge branch 'feat/apify-api-tools' into feat/apify-api-write
DaveHanns Sep 28, 2026
1c32548
fix: Name the method and paths in the read and waitForFinish refusals
DaveHanns Sep 28, 2026
a50574f
fix: Key the API refusals by method and path and refuse browser-info
DaveHanns Sep 28, 2026
a45b999
fix: Time out the API spec download and never cache an empty index
DaveHanns Sep 28, 2026
36e6f2f
fix: Redact the apify-api-details path and wrong-type arguments in logs
DaveHanns Sep 28, 2026
b4d0b7c
test: Remove a duplicated apify-api-details test
DaveHanns Sep 28, 2026
0f22829
fix: Name the parameters that narrow a response over the inline limit
DaveHanns Sep 28, 2026
9905ce7
fix: Leave the refused body fields out of the apify-api-details schema
DaveHanns Sep 28, 2026
a3bb7b4
fix: Send the agent to apify-api-details with the method, not only th…
DaveHanns Sep 28, 2026
4f3aca2
docs: Describe the path-keyed API rules and the details redaction in …
DaveHanns Sep 28, 2026
38fb7e8
Merge branch 'feat/apify-api-tools' into feat/apify-api-write
DaveHanns Sep 28, 2026
9408b34
fix: Key the Actor source and version refusals by method and path
DaveHanns Sep 28, 2026
e385fb9
fix: Require isSecret when apify-api-write replaces an env var
DaveHanns Sep 28, 2026
e160d30
fix: Report the status of a failed write whose error body is over the…
DaveHanns Sep 28, 2026
441568d
fix: Mark apify-api-write as open-world
DaveHanns Sep 28, 2026
3ecf072
fix: Describe the apify-api-write body as any JSON value but a string…
DaveHanns Sep 28, 2026
20a1775
fix: Send the agent to Apify Console, not to source tools that do not…
DaveHanns Sep 28, 2026
ee969f4
fix: Send the agent to apify-api-details with the method from apify-a…
DaveHanns Sep 28, 2026
936ed1f
refactor: Join the version-create reason into one string
DaveHanns Sep 28, 2026
e11d6cd
test: Give the write fixture's version and run paths their real methods
DaveHanns Sep 28, 2026
c1aa14f
docs: Rewrap the api paragraph in src/tools/AGENTS.md
DaveHanns Sep 28, 2026
bc958d3
refactor: Make apify-api-read a proxy to the API, like apify api
DaveHanns Sep 29, 2026
7102aee
Merge branch 'feat/apify-api-tools' into feat/apify-api-write
DaveHanns Sep 29, 2026
14de78b
fix: Look up acts paths as actors and keep secrets out of API errors
DaveHanns Sep 29, 2026
015842a
Merge branch 'feat/apify-api-tools' into feat/apify-api-write
DaveHanns Sep 29, 2026
6ebf613
fix: Ask for the method when the spec cannot be loaded to choose it
DaveHanns Sep 29, 2026
b7b0fd1
feat: Index PATCH operations in the API spec
DaveHanns Sep 29, 2026
b3387ce
Merge branch 'feat/apify-api-tools' into feat/apify-api-write
DaveHanns Sep 29, 2026
0aaf1ee
feat: Send PATCH from apify-api-write, and never a GET
DaveHanns Sep 29, 2026
4fd1838
feat: Serve apify-api-search and apify-api-details without a token
DaveHanns Sep 29, 2026
d06cc7a
Merge branch 'feat/apify-api-tools' into feat/apify-api-write
DaveHanns Sep 29, 2026
a10b7fc
chore: Merge master into feat/apify-api-tools
DaveHanns Oct 2, 2026
3e6be78
refactor: Reuse the API resource's request helpers in the API tools
DaveHanns Oct 2, 2026
8b011ac
docs: Say that the API resource gets the same headers as the API tools
DaveHanns Oct 2, 2026
2026b20
refactor: Send the API tools' requests with the API resource's reques…
DaveHanns Oct 2, 2026
5c1a53c
test: Cover the API tools' origin check and pin the API origin
DaveHanns Oct 2, 2026
cd18359
chore: Merge feat/apify-api-tools into feat/apify-api-write
DaveHanns Oct 2, 2026
330c8bc
refactor: Send apify-api-write's JSON body through the API resource's…
DaveHanns Oct 2, 2026
3e516d9
docs: Teach ~name for your own Actors and storages in the API tools
DaveHanns Oct 6, 2026
8a7b835
fix: Send a JSON string body as the value it holds in apify-api-write
DaveHanns Oct 6, 2026
720b3ef
docs: Teach ~name for any resource you own in the API tools
DaveHanns Oct 6, 2026
da58fa3
Merge branch 'feat/apify-api-tools' into feat/apify-api-write
DaveHanns Oct 6, 2026
8aa2922
feat: Search API operations by words only their description has
DaveHanns Oct 6, 2026
ad88b79
Merge branch 'feat/apify-api-tools' into feat/apify-api-write
DaveHanns Oct 6, 2026
543053f
fix: Address review of the API tools
DaveHanns Oct 6, 2026
05bd3a4
Merge branch 'feat/apify-api-tools' into feat/apify-api-write
DaveHanns Oct 6, 2026
0bcbf07
fix: Address the second review of the API tools
DaveHanns Oct 6, 2026
d35e000
Merge branch 'feat/apify-api-tools' into feat/apify-api-write
DaveHanns Oct 6, 2026
57d8a09
fix: Mark apify-api-read as open-world
claude Oct 7, 2026
32c39bc
fix: Keep the API's punctuation before the closest paths on a 404
claude Oct 7, 2026
4b7ef87
feat: Serve apify-api-write only when a session names it
DaveHanns Oct 8, 2026
8a61b5e
fix: Ask apify-api-write for only the write methods of a path
DaveHanns Oct 8, 2026
786287e
feat: Check the API tools' calls against a typed blocklist
DaveHanns Oct 8, 2026
c5a4842
fix: Refuse the method query parameter in the API call tools
DaveHanns Oct 8, 2026
00be483
fix: Refuse the synchronous run endpoints in the API tools
DaveHanns Oct 8, 2026
0ee6b7d
fix: Annotate apify-api-read as read-only and idempotent
DaveHanns Oct 8, 2026
9992af9
test: Run an operation rule through apify-api-read and apify-api-write
DaveHanns Oct 8, 2026
002ed88
Merge branch 'fix/api-read-method-override' into fix/api-read-run-sync
DaveHanns Oct 8, 2026
6fd5eb5
fix: Refuse only the GET of the run-sync endpoints in apify-api-read
DaveHanns Oct 8, 2026
cb4e093
fix: Refuse in apify-api-details only the method given on a path the …
DaveHanns Oct 8, 2026
9945489
docs: Say apify-api-details refuses a path only when the lookup has n…
DaveHanns Oct 8, 2026
f7fec24
docs: Restore why apify-api-read redacts the webhooks of a synchronou…
DaveHanns Oct 8, 2026
5465ec2
test: Name the run-sync path blocklist case for the GET rule it checks
DaveHanns Oct 8, 2026
052b22e
docs: Reword the apify-api-read refusals in the README tool table
DaveHanns Oct 8, 2026
6e892ef
refactor: Simplify the API blocklist
DaveHanns Oct 8, 2026
c99c193
Merge branch 'fix/api-read-method-override' into fix/api-read-run-sync
DaveHanns Oct 8, 2026
991051f
test: Adapt the run-sync tests to the simplified blocklist
DaveHanns Oct 8, 2026
9bdb2a4
refactor: Check the match kind only where a blocklist rule can be a q…
DaveHanns Oct 8, 2026
9bef709
fix: Ask apify-api-write for the method when a rule blocks another wr…
DaveHanns Oct 8, 2026
a170752
docs: Say which blocklist matches only fail closed
DaveHanns Oct 8, 2026
912b786
docs: Shorten the API blocklist paragraph in the tools AGENTS.md
DaveHanns Oct 8, 2026
4c37704
test: Keep one apify-api-read case per way the method query parameter…
DaveHanns Oct 8, 2026
ebf0be3
Merge branch 'fix/api-read-method-override' into fix/api-read-run-sync
DaveHanns Oct 8, 2026
8438037
docs: Shorten the comment on the run-sync blocklist rules
DaveHanns Oct 8, 2026
0604b0f
test: Keep one run-sync case per wiring path in the tool and index tests
DaveHanns Oct 8, 2026
7e1f2f5
fix: Refuse a path that climbs out of /v2/ in the API call tools
DaveHanns Oct 8, 2026
7a98272
Merge branch 'fix/api-read-method-override' into fix/api-read-run-sync
DaveHanns Oct 8, 2026
8e67b80
Merge branch 'feat/apify-api-tools' into feat/apify-api-write
DaveHanns Oct 9, 2026
ba41a41
Merge remote-tracking branch 'origin/master' into feat/apify-api-write
DaveHanns Oct 9, 2026
b0e49fe
Merge branch 'feat/apify-api-write' into fix/api-read-method-override
DaveHanns Oct 9, 2026
b7155d4
test: Pin the /v2/ path check and the blocked GET case of apify-api-w…
DaveHanns Oct 9, 2026
511f8c0
Merge branch 'fix/api-read-method-override' into fix/api-read-run-sync
DaveHanns Oct 9, 2026
608882b
fix: Mark apify-api-read as not open-world again
DaveHanns Oct 9, 2026
c2a66f1
fix: End the run-sync sentence of apify-api-read at "is refused."
DaveHanns Oct 9, 2026
3654dec
fix: Add the HTTP status hint to a failed write with an oversize erro…
DaveHanns Oct 9, 2026
46977a5
Merge branch 'feat/apify-api-write' into fix/api-read-method-override
DaveHanns Oct 9, 2026
27d79b6
Merge branch 'fix/api-read-method-override' into fix/api-read-run-sync
DaveHanns Oct 9, 2026
1f26f1f
refactor: Name the API blocklist rule kinds endpoint rule and query rule
DaveHanns Oct 9, 2026
d732bf8
test: Match a blocklist rule path written without /v2 as its /v2 form
DaveHanns Oct 9, 2026
70207bf
refactor: Add isQueryBlocked to check query parameter names against t…
DaveHanns Oct 9, 2026
2add2f7
Merge branch 'fix/api-read-method-override' into fix/api-read-run-sync
DaveHanns Oct 9, 2026
0f991cb
refactor: Check query rules in isQueryBlocked through isRuleMatch
DaveHanns Oct 9, 2026
dbf9ea0
refactor: Move normalizeApiPath to apify_api_path.ts
DaveHanns Oct 9, 2026
39ee9f9
test: Read a blocklist rule path written with acts as actors
DaveHanns Oct 9, 2026
84b83cc
Merge branch 'fix/api-read-method-override' into fix/api-read-run-sync
DaveHanns Oct 9, 2026
11b5dec
test: Import normalizeApiPath from apify_api_path.ts in the blocklist…
DaveHanns Oct 9, 2026
97b1605
refactor: Read legacy path prefixes from one alias table
DaveHanns Oct 9, 2026
ffe850b
Merge branch 'fix/api-read-method-override' into fix/api-read-run-sync
DaveHanns Oct 9, 2026
d23eb25
refactor: Name the alias table API_RESOURCE_ALIASES
DaveHanns Oct 9, 2026
6d5008e
Merge branch 'fix/api-read-method-override' into fix/api-read-run-sync
DaveHanns Oct 9, 2026
58606cf
refactor: Keep isRuleMatch strict and tell the rule kinds apart with …
DaveHanns Oct 9, 2026
342cda2
Merge branch 'fix/api-read-method-override' into fix/api-read-run-sync
DaveHanns Oct 9, 2026
722f9cf
Merge remote-tracking branch 'origin/master' into feat/apify-api-write
DaveHanns Oct 12, 2026
f159de9
refactor: Return early from the oversize branch of callApi's catch block
DaveHanns Oct 12, 2026
2def4c5
refactor: Rename inferMethod to inferWriteMethod and ask for the meth…
DaveHanns Oct 12, 2026
1a28649
Merge branch 'feat/apify-api-write' into fix/api-read-method-override
DaveHanns Oct 12, 2026
1eabae5
test: Expect the one-sentence method prompt when a rule blocks anothe…
DaveHanns Oct 12, 2026
64d0729
refactor: Say in inferWriteMethod's docstring that a blocked write me…
DaveHanns Oct 12, 2026
993ebfd
Merge branch 'fix/api-read-method-override' into fix/api-read-run-sync
DaveHanns Oct 12, 2026
17d1e87
refactor: Make inferWriteMethod's docstring match its only-GET refusal
DaveHanns Oct 12, 2026
cd31a6e
fix: Say the write tool returns the parsed response body, as the read…
DaveHanns Oct 12, 2026
1e57dea
test: Pin HTTP 300 as a failed oversize write
DaveHanns Oct 12, 2026
95606ed
Merge branch 'feat/apify-api-write' into fix/api-read-method-override
DaveHanns Oct 12, 2026
a9f84ca
Merge branch 'fix/api-read-method-override' into fix/api-read-run-sync
DaveHanns Oct 12, 2026
a51404c
Merge remote-tracking branch 'origin/master' into fix/api-read-method…
DaveHanns Oct 12, 2026
2054b7f
docs: Say validateApiBlocklist also refuses a path that leaves /v2/
DaveHanns Oct 12, 2026
0aedaa0
Merge branch 'fix/api-read-method-override' into fix/api-read-run-sync
DaveHanns Oct 12, 2026
5f6469c
refactor: Remove isQueryBlocked, which has no caller
DaveHanns Oct 12, 2026
13966ec
Merge branch 'fix/api-read-method-override' into fix/api-read-run-sync
DaveHanns Oct 12, 2026
169802e
docs: Make the validateApiBlocklist docstring plain
DaveHanns Oct 12, 2026
49437f1
Merge branch 'fix/api-read-method-override' into fix/api-read-run-sync
DaveHanns Oct 12, 2026
04a1fac
Merge remote-tracking branch 'origin/master' into fix/api-read-run-sync
DaveHanns Oct 12, 2026
7fd3782
fix: Advise asynchronous runs only in apify-api-write
DaveHanns Oct 12, 2026
a8f3cd2
fix: Refuse in resources/read what the API tools refuse
DaveHanns Oct 12, 2026
95520f6
Merge remote-tracking branch 'origin/master' into fix/resource-read-b…
DaveHanns Oct 13, 2026
9074b7d
Merge remote-tracking branch 'origin/master' into fix/resource-read-b…
claude Oct 15, 2026
0d8d93e
test: Check the resources/read refusal text names no tool
claude Oct 15, 2026
32cab6a
fix: Clarify method override refusal
jirispilka Oct 15, 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
6 changes: 4 additions & 2 deletions src/resources/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ to two origins — the one clients are told to use (`getApifyAPIPublicBaseUrl()`
and the egress one (`getApifyAPIBaseUrl()`), which differ on the hosted server — and rejects
userinfo-bearing URLs (axios drops the `Authorization` header for those, silently degrading to
unauthenticated). An accepted URI is sent on the egress origin and returned under the URI as given;
templates, instructions, refusals and link-out URLs name only the client-facing origin. The API tools' `callApi`
(`../tools/api/apify_api_request.ts`) reuses `isApifyApiUri()`, `isMaxContentLengthAbort()`,
templates, instructions, refusals and link-out URLs name only the client-facing origin. `readApiResource()`
then refuses, without a request, what the API tools refuse (`validateApiBlocklist` in
`../tools/api/apify_api_blocklist.ts`): the `method` query parameter and a GET to the synchronous run
endpoints. The API tools' `callApi` (`../tools/api/apify_api_request.ts`) reuses `isApifyApiUri()`, `isMaxContentLengthAbort()`,
`maskSessionToken()`, `redactJsonSecrets()` and `REDACTED`, so a change to any changes the
tools too. `get-key-value-store-record` builds its inlined-record URI with `buildUrlOnOrigin()`.

Expand Down
15 changes: 13 additions & 2 deletions src/resources/api_resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import type { ApifyClient } from '../apify_client.js';
import { getApifyAPIBaseUrl, getApifyAPIPublicBaseUrl, sendApifyApiRequest } from '../apify_client.js';
import { MAX_INLINE_BYTES } from '../const.js';
import { InternalError, InvalidParamsError } from '../mcp/errors.js';
import { validateApiBlocklist } from '../tools/api/apify_api_blocklist.js';
import { normalizeApiPath } from '../tools/api/apify_api_path.js';
import { parseBaseMimeType } from '../tools/storage/storage_helpers.js';
import { getHttpStatusCode, logHttpError } from '../utils/logging.js';
import { getHttpErrorHint } from '../utils/mcp.js';
Expand Down Expand Up @@ -208,8 +210,8 @@ export function redactJsonSecrets(json: string): string {
* JSON primitives, formatting, and bytes round-trip exactly, except that the session token and
* `urlSigningSecretKey` and `password` values in JSON become `[REDACTED]`.
*
* Genuine failures (no token, bad origin, a missing resource, a bad token, a 5xx, a network error)
* throw a domain error (`InvalidParamsError`/`InternalError`) that the protocol adapters
* Genuine failures (no token, bad origin, a request the API tools refuse, a missing resource, a bad token,
* a 5xx, a network error) throw a domain error (`InvalidParamsError`/`InternalError`) that the protocol adapters
* (`legacy_server.ts`, `stateless_server.ts`) map to a JSON-RPC error, so the SDK returns an error
* rather than success-shaped content for an unreadable resource (see SEP-2164). A body over
* `MAX_INLINE_BYTES` is NOT a failure — it is a successful read returning a download pointer.
Expand All @@ -229,6 +231,15 @@ export async function readApiResource(uri: string, apifyClient?: ApifyClient): P
uri,
});
}
// A GET with the method query parameter can delete, and one to a synchronous run endpoint starts a paid run;
// see apify/apify-mcp-server#1524.
const { pathname, search } = new URL(uri);
const refusal = validateApiBlocklist({
method: 'GET',
normalizedPath: normalizeApiPath(`${pathname}${search}`),
loadedToolNames: [],
});
if (refusal) throw new InvalidParamsError(`Failed to read ${uri}: ${refusal}`, { uri });

let response: { data: unknown; headers: Record<string, unknown>; status: number; statusText: string };
try {
Expand Down
4 changes: 1 addition & 3 deletions src/tools/api/apify_api_blocklist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ export const API_BLOCK_RULES: readonly ApiBlockRule[] = [
{
// A GET with method=DELETE deletes, so the read tool could write; see apify/apify-mcp-server#1501.
match: { queryParam: 'method' },
reason:
'The API tools do not send the method query parameter: the API would take it as the HTTP method ' +
'of the request.',
reason: 'The method query parameter is not allowed because it can turn a read request into a write request.',
suggestedToolNames: [HELPER_TOOLS.API_WRITE],
},
// A GET here starts a paid run, so the read tool would not be read-only; see apify/apify-mcp-server#1502.
Expand Down
23 changes: 23 additions & 0 deletions tests/unit/resources.api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import { mockApifyClient } from './helpers/tool_context.js';

const API = 'https://api.apify.com';

const METHOD_PARAM_REFUSAL =
'The method query parameter is not allowed because it can turn a read request into a write request.';

const SYNC_RUN_REFUSAL =
'A GET to the synchronous run endpoints starts a paid run, as a POST does, so the API tools do not send it.';

// `contents[0]` is a text|blob union; narrow it in tests that read one shape.
function firstContent(result: ReadResourceResult): { mimeType?: string; text?: string; blob?: string } {
return result.contents[0] as { mimeType?: string; text?: string; blob?: string };
Expand Down Expand Up @@ -205,6 +211,23 @@ describe('readApiResource()', () => {
expect(error.data).toEqual({ uri: 'https://example.com/steal-my-token' });
});

it.each([
[`${API}/v2/datasets/ds-1?method=DELETE`, METHOD_PARAM_REFUSAL],
[`${API}/v2/datasets/ds-1?%6Dethod=DELETE`, METHOD_PARAM_REFUSAL],
[`${API}/v2/acts/apify~hello-world/run-sync`, SYNC_RUN_REFUSAL],
[`${API}/v2/actor-tasks/abc/run-sync-get-dataset-items`, SYNC_RUN_REFUSAL],
])('throws InvalidParams without a request for %s, as the API tools refuse it', async (uri, refusal) => {
const { request, captured } = requestReturning(streamOf('{}'), 'application/json');

const error = await expectReadError(readApiResource(uri, stubApifyClient({ request })));

expect(error).toBeInstanceOf(InvalidParamsError);
// readApiResource does not know the session's tools, so the refusal names none.
expect(error.message).toBe(`Failed to read ${uri}: ${refusal}`);
expect(error.data).toEqual({ uri });
expect(captured.url).toBe('');
});

it('requests the full URL as a stream capped at MAX_INLINE_BYTES', async () => {
// The size cap is enforced by axios itself mid-consumption (streamed responses are wrapped in a
// byte-counting generator since axios 1.16), so the contract under test is the request config.
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/tools.apify_api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const SYNC_RUN_REFUSAL =
'A GET to the synchronous run endpoints starts a paid run, as a POST does, so the API tools do not send it.';

const METHOD_PARAM_REFUSAL =
'The API tools do not send the method query parameter: the API would take it as the HTTP method of the request.';
'The method query parameter is not allowed because it can turn a read request into a write request.';

/** The URL axios sends for the last request, with the query parameters added. */
function readSentUrl(): string {
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/tools.apify_api_blocklist.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { normalizeApiPath } from '../../src/tools/api/apify_api_path.js';
import { API_METHODS } from '../../src/tools/api/apify_api_spec.js';

const METHOD_PARAM_REFUSAL =
'The API tools do not send the method query parameter: the API would take it as the HTTP method of the request.';
'The method query parameter is not allowed because it can turn a read request into a write request.';

const SYNC_RUN_REFUSAL =
'A GET to the synchronous run endpoints starts a paid run, as a POST does, so the API tools do not send it.';
Expand Down
Loading