Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,6 @@ Legend for the **Enabled by default** column:
| `search-actors` | actors | Search for Actors in Apify Store. | ✅ |
| `fetch-actor-details` | actors | Retrieve detailed information about a specific Actor, including its input schema, README (summary when available, full otherwise), pricing, and Actor output schema. | ✅ |
| `call-actor` | actors | Call an Actor and get its run results. Use fetch-actor-details first to get the Actor's input schema. | ✅ |
| `get-actor-list` | actors | List the Actors you own and those shared with you, including private ones. | ✅ |
| `get-actor-run` | runs | Get detailed information about a specific Actor run. | ⚡ |
| `get-dataset-items` | storage | Retrieve items from a dataset with support for filtering and pagination. | ⚡ |
| `get-key-value-store-record`| storage | Get the value associated with a specific key in a key-value store. | ⚡ |
Expand All @@ -274,6 +273,7 @@ Legend for the **Enabled by default** column:
| [`apify--rag-web-browser`](https://apify.com/apify/rag-web-browser) | Actor (see [tool configuration](#tools-configuration)) | An Actor tool to browse the web. | ✅ |
| [`apify--web-fetch`](https://apify.com/apify/web-fetch) | Actor (see [tool configuration](#tools-configuration)) | An Actor tool to fetch a URL and return its content. | ✅ |
| `report-problem` | dev | Report a problem with an Apify tool or Actor to the Apify team. | ✅¹ |
| `get-actor-list` | none (select by name: `tools=get-actor-list`) | List the Actors you own and those shared with you, including private ones. | |
| `get-actor-run-list` | runs | Get a list of Actor runs, filterable by Actor and status. | |
| `get-actor-run-log` | runs | Retrieve the logs for a specific Actor run. | |
| `get-dataset` | storage | Get metadata about a specific dataset. | |
Expand Down
2 changes: 2 additions & 0 deletions src/index_internals_test_kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SKYFIRE_ENABLED_TOOLS } from './payments/const.js';
import { RESOURCE_MIME_TYPE } from './resources/widgets.js';
import { CALL_ACTOR_MCP_MISSING_TOOL_NAME_MSG } from './tools/actors/call_actor.js';
import { toolCategoriesEnabledByDefault } from './tools/index.js';
import { UNCATEGORIZED_TOOLS } from './tools/registry.js';
import { actorRunOutputSchema } from './tools/structured_output_schemas.js';
import type { SERVER_MODE, TelemetryEnv, ToolEntry } from './types.js';
import { APIFY_ACTOR_RUN_META_KEY } from './utils/mcp.js';
Expand All @@ -22,6 +23,7 @@ export {
RESOURCE_MIME_TYPE,
CALL_ACTOR_MCP_MISSING_TOOL_NAME_MSG,
toolCategoriesEnabledByDefault,
UNCATEGORIZED_TOOLS,
actorRunOutputSchema,
type SERVER_MODE,
type TelemetryEnv,
Expand Down
3 changes: 2 additions & 1 deletion src/tools/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ direct actor tools, `search-actors`, `fetch-actor-details`) is mode-agnostic.

## Files

- `registry.ts` — tool categories and the tools in each (`index.ts` re-exports them).
- `registry.ts` — tool categories and the tools in each (`index.ts` re-exports them), plus tools in no
category (`ALL_WIDGET_TOOLS`, `UNCATEGORIZED_TOOLS`).
- `structured_output_schemas.ts` — shared JSON-schema definitions for structured
output across tools.
- `utils.ts` — shared tool helpers (schema property shaping, AJV compile).
Expand Down
8 changes: 7 additions & 1 deletion src/tools/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ import { searchActorsWidget } from './widgets/search_actors_widget.js';

/** Unified tool category definitions — single source of truth. */
export const toolCategories = {
actors: [searchActors, fetchActorDetails, callActor, getActorList],
actors: [searchActors, fetchActorDetails, callActor],
docs: [searchApifyDocs, fetchApifyDocs],
runs: [getActorRun, getActorRunList, getActorRunLog, abortActorRun],
storage: [
Expand Down Expand Up @@ -101,6 +101,12 @@ export const ALL_WIDGET_TOOLS: readonly ToolEntry[] = [
getActorRunWidget,
];

/**
* Non-widget tools in no category: never served by default or by a category, only when named in
* `tools=`, in every mode. Temporary: `get-actor-list` returns to a category once evals cover it.
*/
export const UNCATEGORIZED_TOOLS: readonly ToolEntry[] = [getActorList];

/**
* Apps-mode auto-pairing: a widget is added iff its base tool is present — see
* `getToolsForServerMode` in tools_loader.ts. `call-actor`/`get-actor-run` widgets don't pair (low
Expand Down
3 changes: 3 additions & 0 deletions src/utils/tools_loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
CATEGORY_NAMES,
getCategoryTools,
toolCategoriesEnabledByDefault,
UNCATEGORIZED_TOOLS,
WIDGET_BY_BASE_TOOL,
} from '../tools/registry.js';
import { abortActorRun } from '../tools/runs/abort_actor_run.js';
Expand Down Expand Up @@ -48,6 +49,7 @@ const ALL_INTERNAL_TOOL_NAMES: Set<string> = (() => {
for (const name of CATEGORY_NAMES) {
for (const tool of categories[name]) names.add(tool.name);
}
for (const tool of UNCATEGORIZED_TOOLS) names.add(tool.name);
// Widgets live in no category — ALL_WIDGET_TOOLS covers every widget, paired or not.
for (const widget of ALL_WIDGET_TOOLS) names.add(widget.name);
return names;
Expand Down Expand Up @@ -208,6 +210,7 @@ export function getToolsForServerMode(
toolsByName.set(tool.name, tool);
}
}
for (const tool of UNCATEGORIZED_TOOLS) toolsByName.set(tool.name, tool);
// Widgets are apps-only and not in any category; include every widget (paired or not) for
// direct `?tools=` selection.
if (mode === SERVER_MODE.APPS) {
Expand Down
4 changes: 2 additions & 2 deletions tests/test_kit/cases/registration.cases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export const registrationCases: Case[] = [
const names = getToolNames(tools);

// Equivalent to tools=actors,docs,apify/rag-web-browser,apify/web-fetch (no widgets outside apps).
const expectedActorsTools = ['fetch-actor-details', 'search-actors', 'call-actor', 'get-actor-list'];
const expectedActorsTools = ['fetch-actor-details', 'search-actors', 'call-actor'];
const expectedDocsTools = ['search-apify-docs', 'fetch-apify-docs'];
const expectedActors = [
actorNameToToolName('apify/rag-web-browser'),
Expand All @@ -103,7 +103,7 @@ export const registrationCases: Case[] = [
isDeploymentTest: true,
run: withClient({ telemetry: { enabled: true } }, async (client) => {
const names = getToolNames(await client.listTools());
expect(names).toHaveLength(9 + AUTO_INJECTED_TOOL_NAMES.length);
expect(names).toHaveLength(8 + AUTO_INJECTED_TOOL_NAMES.length);
expect(names).toContain(HELPER_TOOLS.PROBLEM_REPORT);
}),
},
Expand Down
3 changes: 2 additions & 1 deletion tests/test_kit/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
AUTO_INJECTED_TOOLS,
HELPER_TOOLS,
toolCategoriesEnabledByDefault,
UNCATEGORIZED_TOOLS,
} from '@apify/actors-mcp-server/internals/test-kit.js';

import type { CaseCtx, SuiteClient, Transport } from './types.js';
Expand Down Expand Up @@ -128,7 +129,7 @@ export function findToolByName(name: string, mode: SERVER_MODE): ToolEntry | und
const tool = tools.find((t) => t.name === name);
if (tool) return tool;
}
return undefined;
return UNCATEGORIZED_TOOLS.find((t) => t.name === name);
}

export function validateStructuredOutputForTool(result: unknown, toolName: string, mode: SERVER_MODE): void {
Expand Down
1 change: 0 additions & 1 deletion tests/unit/server_card.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ const EXPECTED_TOOL_NAMES = [
'get-dataset-items',
'get-key-value-store-record',
'abort-actor-run',
'get-actor-list',
'search-apify-docs',
'fetch-apify-docs',
];
Expand Down
3 changes: 1 addition & 2 deletions tests/unit/tools.categories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,11 @@ describe('getCategoryTools', () => {
const result = getCategoryTools('default');
const actorNames = result.actors.map((t: ToolEntry) => t.name);

// Verify order: the search → details → call workflow first, then the account's own Actors
// Verify workflow order: search → details → call
expect(actorNames).toEqual([
HELPER_TOOLS.STORE_SEARCH,
HELPER_TOOLS.ACTOR_GET_DETAILS,
HELPER_TOOLS.ACTOR_CALL,
HELPER_TOOLS.ACTOR_LIST_GET,
]);
});
});
21 changes: 14 additions & 7 deletions tests/unit/tools.mode_contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { fetchActorDetails } from '../../src/tools/actors/fetch_actor_details.js
import { searchActorsBaseArgsSchema } from '../../src/tools/actors/search_actors.js';
import { searchApifyDocs } from '../../src/tools/docs/search_apify_docs.js';
import { CATEGORY_NAMES, getCategoryTools } from '../../src/tools/index.js';
import { ALL_WIDGET_TOOLS, WIDGET_BY_BASE_TOOL } from '../../src/tools/registry.js';
import { ALL_WIDGET_TOOLS, UNCATEGORIZED_TOOLS, WIDGET_BY_BASE_TOOL } from '../../src/tools/registry.js';
import type { ActorInfo, Input, ToolBase, ToolEntry } from '../../src/types.js';
import { SERVER_MODES, SERVER_MODE } from '../../src/types.js';
import { getToolPublicFieldOnly } from '../../src/utils/tools.js';
Expand Down Expand Up @@ -58,12 +58,7 @@ describe('getCategoryTools mode contract (tool-mode separation)', () => {

describe('per-mode tool lists', () => {
it('has correct tools in actors category (both modes)', () => {
const expected = [
HELPER_TOOLS.STORE_SEARCH,
HELPER_TOOLS.ACTOR_GET_DETAILS,
HELPER_TOOLS.ACTOR_CALL,
HELPER_TOOLS.ACTOR_LIST_GET,
];
const expected = [HELPER_TOOLS.STORE_SEARCH, HELPER_TOOLS.ACTOR_GET_DETAILS, HELPER_TOOLS.ACTOR_CALL];
expect(toolNames(defaultCategories.actors)).toEqual(expected);
expect(toolNames(appsCategories.actors)).toEqual(expected);
});
Expand Down Expand Up @@ -263,6 +258,12 @@ describe('getCategoryTools mode contract (tool-mode separation)', () => {
expect(Object.isFrozen(widget)).toBe(true);
});
}

for (const tool of UNCATEGORIZED_TOOLS) {
it(`${tool.name} (no category) should be frozen`, () => {
expect(Object.isFrozen(tool)).toBe(true);
});
}
});

describe('all tool names match HELPER_TOOLS values', () => {
Expand All @@ -285,6 +286,12 @@ describe('getCategoryTools mode contract (tool-mode separation)', () => {
expect(allHelperToolNames.has(widget.name as HelperToolName)).toBe(true);
});
}

for (const tool of UNCATEGORIZED_TOOLS) {
it(`${tool.name} (no category) should be a known HELPER_TOOLS value`, () => {
expect(allHelperToolNames.has(tool.name as HelperToolName)).toBe(true);
});
}
});
});

Expand Down
38 changes: 38 additions & 0 deletions tests/unit/utils.tools_loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { describe, expect, it } from 'vitest';
import { HELPER_TOOLS } from '../../src/const.js';
import type { ToolEntry } from '../../src/types.js';
import { TOOL_TYPE } from '../../src/types.js';
import { getToolPublicFieldOnly } from '../../src/utils/tools.js';
import {
AUTO_INJECTED_TOOLS,
getToolsForServerMode,
isReportProblemExplicitlySelected,
loadToolsFromInput,
resolveActorsToLoad,
resolveToolNamesFromInput,
toolNamesToInput,
} from '../../src/utils/tools_loader.js';
Expand Down Expand Up @@ -109,6 +111,12 @@ describe('toolNamesToInput', () => {
tools: [HELPER_TOOLS.STORE_SEARCH_WIDGET],
});
});

it('classifies get-actor-list (in no category) as an internal tool, not an actor ID', () => {
expect(toolNamesToInput([HELPER_TOOLS.ACTOR_LIST_GET])).toEqual({
tools: [HELPER_TOOLS.ACTOR_LIST_GET],
});
});
});

describe('storage tool auto-injection', () => {
Expand Down Expand Up @@ -220,6 +228,36 @@ describe('getToolsForServerMode report-problem default injection', () => {
});
});

describe('get-actor-list selection (in no category)', () => {
it('does not treat tools=get-actor-list as an Actor name', () => {
expect(resolveActorsToLoad({ tools: [HELPER_TOOLS.ACTOR_LIST_GET] })).toEqual([]);
});

it.each(['default', 'apps'] as const)('serves only get-actor-list for tools=get-actor-list in %s mode', (mode) => {
const toolNames = getToolsForServerMode({ tools: [HELPER_TOOLS.ACTOR_LIST_GET] }, [], mode).map((t) => t.name);
expect(toolNames).toEqual([HELPER_TOOLS.ACTOR_LIST_GET]);
});

it.each(['default', 'apps'] as const)('excludes get-actor-list from the default set in %s mode', (mode) => {
const toolNames = getToolsForServerMode({}, [], mode).map((t) => t.name);
expect(toolNames).not.toContain(HELPER_TOOLS.ACTOR_LIST_GET);
});

it.each(['default', 'apps'] as const)(
'serves the actors tools and get-actor-list for tools=actors,get-actor-list in %s mode',
(mode) => {
const tools = getToolsForServerMode({ tools: ['actors', HELPER_TOOLS.ACTOR_LIST_GET] }, [], mode);
const toolNames = tools.map((t) => t.name);
const actorsToolNames = getToolsForServerMode({ tools: ['actors'] }, [], mode).map((t) => t.name);
expect(toolNames).toEqual(expect.arrayContaining([...actorsToolNames, HELPER_TOOLS.ACTOR_LIST_GET]));

const searchActorsTool = tools.find((t) => t.name === HELPER_TOOLS.STORE_SEARCH);
const { description } = getToolPublicFieldOnly(searchActorsTool!, { presentTools: new Set(toolNames) });
expect(description).toContain(`Use ${HELPER_TOOLS.ACTOR_LIST_GET} for those.`);
},
);
});

describe('loadToolsFromInput explicit widget selection', () => {
const apifyClient = new ApifyClient({ token: 'test-token' });

Expand Down
Loading