Skip to content
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,10 @@ Here is an overview list of all the tools provided by the Apify MCP Server.

| Tool name | Category | Description | Enabled by default |
| :--- | :--- | :--- | :---: |
| `get-actor-details` | default | Retrieve detailed information about a specific Actor. | ✅ |
| `search-actors` | default | Search for Actors in the Apify Store. | ✅ |
| `add-actor` | default | Add an Actor as a new tool for the user to call. | ✅ |
| [`apify-slash-rag-web-browser`](https://apify.com/apify/rag-web-browser) | default | An Actor tool to browse the web. | ✅ |
| `get-actor-details` | actor-discovery | Retrieve detailed information about a specific Actor. | ✅ |
| `search-actors` | actor-discovery | Search for Actors in the Apify Store. | ✅ |
| `add-actor` | default (see note below) | Add an Actor as a new tool for the user to call. | ✅ |
| [`apify-slash-rag-web-browser`](https://apify.com/apify/rag-web-browser) | Actor (see note below) | An Actor tool to browse the web. | ✅ |
| `search-apify-docs` | docs | Search the Apify documentation for relevant pages. | ✅ |
| `fetch-apify-docs` | docs | Fetch the full content of an Apify documentation page by its URL. | ✅ |
| `call-actor` | preview | Call an Actor and get its run results. | |
Expand All @@ -178,6 +178,10 @@ Here is an overview list of all the tools provided by the Apify MCP Server.
| `get-dataset-list` | storage | List all available datasets for the user. | |
| `get-key-value-store-list`| storage | List all available key-value stores for the user. | |

> **Note:**
> The `add-actor` tool is always enabled by default and does not explicitly belong to any category. Currently, it can be disabled by setting `?enableAddingActors=false` or `--enable-adding-actors false`.
> The `apify-slash-rag-web-browser` is an Apify Actor tool loaded by default. You can disable it by loading a different set of Actors using `?actors=other/actor` or `--actors other/actor`, or you can disable pre-loading of Actors by setting `?actors=` or `--actors=` (to an empty string).

### Prompts

The server provides a set of predefined example prompts to help you get started interacting with Apify through MCP. For example, there is a `GetLatestNewsOnTopic` prompt that allows you to easily retrieve the latest news on a specific topic using the [RAG Web Browser](https://apify.com/apify/rag-web-browser) Actor.
Expand Down
5 changes: 3 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
"title": "Enabled tool categories",
"description": "A comma-separated list of tool categories to enable. Available options: docs, runs, storage, preview.",
"required": false,
"default": "docs"
"default": "actor-discovery,docs"
},
"actors": {
"type": "string",
Expand All @@ -80,7 +80,8 @@
"type": "boolean",
"title": "Enable dynamic Actor adding",
"description": "Allow dynamically adding Actors as tools based on user requests during a session.",
"default": true
"required": false,
"default": false
}
},
"compatibility": {
Expand Down
25 changes: 4 additions & 21 deletions src/actor/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import express from 'express';
import log from '@apify/log';

import { ActorsMcpServer } from '../mcp/server.js';
import { parseInputParamsFromUrl } from '../mcp/utils.js';
import { getHelpMessage, HEADER_READINESS_PROBE, Routes, TransportType } from './const.js';
import { getActorRunData } from './utils.js';

Expand Down Expand Up @@ -80,16 +79,8 @@ export function createExpressApp(

// Load MCP server tools
const apifyToken = process.env.APIFY_TOKEN as string;
const input = parseInputParamsFromUrl(req.url);
if (input.actors || input.enableAddingActors || input.tools) {
log.debug('Loading tools from URL', { sessionId: transport.sessionId, tr: TransportType.SSE });
await mcpServer.loadToolsFromUrl(req.url, apifyToken);
}
// Load default tools if no actors are specified
if (!input.actors) {
log.debug('Loading default tools', { sessionId: transport.sessionId, tr: TransportType.SSE });
await mcpServer.loadDefaultActors(apifyToken);
}
log.debug('Loading tools from URL', { sessionId: transport.sessionId, tr: TransportType.SSE });
await mcpServer.loadToolsFromUrl(req.url, apifyToken);

transportsSSE[transport.sessionId] = transport;
mcpServers[transport.sessionId] = mcpServer;
Expand Down Expand Up @@ -170,16 +161,8 @@ export function createExpressApp(

// Load MCP server tools
const apifyToken = process.env.APIFY_TOKEN as string;
const input = parseInputParamsFromUrl(req.url);
if (input.actors || input.enableAddingActors || input.tools) {
log.debug('Loading tools from URL', { sessionId: transport.sessionId, tr: TransportType.HTTP });
await mcpServer.loadToolsFromUrl(req.url, apifyToken);
}
// Load default tools if no actors are specified
if (!input.actors) {
log.debug('Loading default tools', { sessionId: transport.sessionId, tr: TransportType.HTTP });
await mcpServer.loadDefaultActors(apifyToken);
}
log.debug('Loading tools from URL', { sessionId: transport.sessionId, tr: TransportType.HTTP });
await mcpServer.loadToolsFromUrl(req.url, apifyToken);

// Connect the transport to the MCP server BEFORE handling the request
await mcpServer.connect(transport);
Expand Down
16 changes: 14 additions & 2 deletions src/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,16 @@ export function processInput(originalInput: Partial<Input>): Input {

// actors can be a string or an array of strings
if (input.actors && typeof input.actors === 'string') {
input.actors = input.actors.split(',').map((format: string) => format.trim()) as string[];
/**
* Filter out empty strings to prevent invalid Actor API error.
*/
input.actors = input.actors.split(',').map((format: string) => format.trim()).filter((actor) => actor !== '') as string[];
}
/**
* Replace empty string with empty array to prevent invalid Actor API error.
*/
if (input.actors === '') {
input.actors = [];
}

// enableAddingActors is deprecated, use enableActorAutoLoading instead
Expand All @@ -31,7 +40,10 @@ export function processInput(originalInput: Partial<Input>): Input {
}

if (input.tools && typeof input.tools === 'string') {
input.tools = input.tools.split(',').map((tool: string) => tool.trim()) as ToolCategory[];
/**
* Filter out empty strings just in case.
*/
input.tools = input.tools.split(',').map((tool: string) => tool.trim()).filter((tool) => tool !== '') as ToolCategory[];
}
return input;
}
12 changes: 0 additions & 12 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,6 @@ export class ActorsMcpServer {
this.setupToolHandlers();
this.setupPromptHandlers();

// Add default tools
this.upsertTools(defaultTools);

// Add tools to dynamically load Actors
if (this.options.enableAddingActors) {
this.enableDynamicActorTools();
Expand Down Expand Up @@ -213,7 +210,6 @@ export class ActorsMcpServer {
if (this.toolsChangedHandler) {
this.unregisterToolsChangedHandler();
}
this.upsertTools(defaultTools);
if (this.options.enableAddingActors) {
this.enableDynamicActorTools();
}
Expand Down Expand Up @@ -244,14 +240,6 @@ export class ActorsMcpServer {
}
}

/**
* @deprecated Use `loadDefaultActors` instead.
* Loads default tools if not already loaded.
*/
public async loadDefaultTools(apifyToken: string) {
await this.loadDefaultActors(apifyToken);
}

/**
* Loads tools from URL params.
*
Expand Down
16 changes: 10 additions & 6 deletions src/stdio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,14 @@ Note: Tools that enable you to search Actors from the Apify Store and get their
.parseSync() as CliArgs;

const enableAddingActors = argv.enableAddingActors && argv.enableActorAutoLoading;
const actors = argv.actors as string || '';
const actorList = actors ? actors.split(',').map((a: string) => a.trim()) : [];
// Keys of the tool categories to enable
const toolCategoryKeys = argv.tools ? argv.tools.split(',').map((t: string) => t.trim()) : [];
// Split actors argument, trim whitespace, and filter out empty strings
const actorList = argv.actors !== undefined
? argv.actors.split(',').map((a: string) => a.trim()).filter((a: string) => a.length > 0)
: undefined;
// Split tools argument, trim whitespace, and filter out empty strings
const toolCategoryKeys = argv.tools !== undefined
? argv.tools.split(',').map((t: string) => t.trim()).filter((t: string) => t.length > 0)
: undefined;

// Propagate log.error to console.error for easier debugging
const originalError = log.error.bind(log);
Expand All @@ -114,13 +118,13 @@ async function main() {

// Create an Input object from CLI arguments
const input: Input = {
actors: actorList.length ? actorList : [],
actors: actorList,
enableAddingActors,
tools: toolCategoryKeys as ToolCategory[],
};

// Use the shared tools loading logic
const tools = await loadToolsFromInput(input, process.env.APIFY_TOKEN as string, actorList.length === 0);
const tools = await loadToolsFromInput(input, process.env.APIFY_TOKEN as string);

mcpServer.upsertTools(tools);

Expand Down
20 changes: 14 additions & 6 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Import specific tools that are being used
import type { ToolCategory } from '../types.js';
import { getExpectedToolsByCategories } from '../utils/tools.js';
import { callActor, callActorGetDataset, getActorsAsTools } from './actor.js';
import { getDataset, getDatasetItems, getDatasetSchema } from './dataset.js';
import { getUserDatasetsList } from './dataset_collection.js';
Expand All @@ -14,6 +15,14 @@ import { searchApifyDocsTool } from './search-apify-docs.js';
import { searchActors } from './store_collection.js';

export const toolCategories = {
'actor-discovery': [
getActorDetailsTool,
searchActors,
/**
* TODO: we should add the add-actor tool here but we would need to change the configuraton
* interface around the ?enableAddingActors
*/
],
docs: [
searchApifyDocsTool,
fetchApifyDocsTool,
Expand All @@ -38,16 +47,15 @@ export const toolCategories = {
],
};
export const toolCategoriesEnabledByDefault: ToolCategory[] = [
'actor-discovery',
'docs',
];

export const defaultTools = [
getActorDetailsTool,
searchActors,
// Add the tools from the enabled categories
...toolCategoriesEnabledByDefault.map((key) => toolCategories[key]).flat(),
];
export const defaultTools = getExpectedToolsByCategories(toolCategoriesEnabledByDefault);

/**
* Tools related to `enableAddingActors` param for dynamic Actor adding.
*/
export const addRemoveTools = [
addTool,
];
Expand Down
14 changes: 12 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,12 @@ export interface InternalTool extends ToolBase {
export type ToolCategory = keyof typeof toolCategories;

export type Input = {
actors: string[] | string;
/**
* When `actors` is undefined that means the default Actors should be loaded.
* If it as empty string or empty array then no Actors should be loaded.
* Otherwise the specified Actors should be loaded.
*/
actors?: string[] | string;
/**
* @deprecated Use `enableAddingActors` instead.
*/
Expand All @@ -222,7 +227,12 @@ export type Input = {
maxActorMemoryBytes?: number;
debugActor?: string;
debugActorInput?: unknown;
/** Tool categories to include */
/**
* Tool categories to include
* When `tools` is undefined that means the default tools categories should be loaded.
* If it as empty string or empty array then no tools should be loaded.
* Otherwise the specified tools categories should be loaded.
*/
tools?: ToolCategory[] | string;
};

Expand Down
15 changes: 8 additions & 7 deletions src/utils/tools-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,30 @@
*/

import { defaults } from '../const.js';
import { addRemoveTools, getActorsAsTools, toolCategories } from '../tools/index.js';
import { addRemoveTools, getActorsAsTools, toolCategories, toolCategoriesEnabledByDefault } from '../tools/index.js';
import type { Input, ToolCategory, ToolEntry } from '../types.js';
import { getExpectedToolsByCategories } from './tools.js';

/**
* Load tools based on the provided Input object.
* This function is used by both the stdio.ts and the processParamsGetTools function.
*
* @param input The processed Input object
* @param apifyToken The Apify API token
* @param useDefaultActors Whether to use default actors if no actors are specified
* @returns An array of tool entries
*/
export async function loadToolsFromInput(
input: Input,
apifyToken: string,
useDefaultActors = false,
): Promise<ToolEntry[]> {
let tools: ToolEntry[] = [];

// Load actors as tools
if (input.actors && (Array.isArray(input.actors) ? input.actors.length > 0 : input.actors)) {
if (input.actors !== undefined) {
const actors = Array.isArray(input.actors) ? input.actors : [input.actors];
tools = await getActorsAsTools(actors, apifyToken);
} else if (useDefaultActors) {
// Use default actors if no actors are specified and useDefaultActors is true
} else {
// Use default actors if no actors are specified
tools = await getActorsAsTools(defaults.actors, apifyToken);
}

Expand All @@ -38,12 +37,14 @@ export async function loadToolsFromInput(
}

// Add tools from enabled categories
if (input.tools) {
if (input.tools !== undefined) {
const toolKeys = Array.isArray(input.tools) ? input.tools : [input.tools];
for (const toolKey of toolKeys) {
const keyTools = toolCategories[toolKey as ToolCategory] || [];
tools.push(...keyTools);
}
} else {
tools.push(...getExpectedToolsByCategories(toolCategoriesEnabledByDefault));
}

return tools;
Expand Down
18 changes: 17 additions & 1 deletion src/utils/tools.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ToolBase } from '../types.js';
import { toolCategories } from '../tools/index.js';
import type { ToolBase, ToolCategory, ToolEntry } from '../types.js';

/**
* Returns a public version of the tool containing only fields that should be exposed publicly.
Expand All @@ -11,3 +12,18 @@ export function getToolPublicFieldOnly(tool: ToolBase) {
inputSchema: tool.inputSchema,
};
}

/**
* Returns the tool objects for the given category names using toolCategories.
*/
export function getExpectedToolsByCategories(categories: ToolCategory[]): ToolEntry[] {
return categories
.flatMap((category) => toolCategories[category] || []);
}

/**
* Returns the tool names for the given category names using getExpectedToolsByCategories.
*/
export function getExpectedToolNamesByCategories(categories: ToolCategory[]): string[] {
return getExpectedToolsByCategories(categories).map((tool) => tool.tool.name);
}
5 changes: 3 additions & 2 deletions tests/const.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { toolCategoriesEnabledByDefault } from '../dist/index-internals.js';
import { defaults } from '../src/const.js';
import { defaultTools } from '../src/tools/index.js';
import { actorNameToToolName } from '../src/tools/utils.js';
import { getExpectedToolNamesByCategories } from '../src/utils/tools.js';

export const ACTOR_PYTHON_EXAMPLE = 'apify/python-example';
export const ACTOR_MCP_SERVER_ACTOR_NAME = 'apify/actors-mcp-server';
export const DEFAULT_TOOL_NAMES = defaultTools.map((tool) => tool.tool.name);
export const DEFAULT_TOOL_NAMES = getExpectedToolNamesByCategories(toolCategoriesEnabledByDefault);
export const DEFAULT_ACTOR_NAMES = defaults.actors.map((tool) => actorNameToToolName(tool));
12 changes: 6 additions & 6 deletions tests/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ export async function createMcpSseClient(
}
const url = new URL(serverUrl);
const { actors, enableAddingActors, tools } = options || {};
if (actors) {
if (actors !== undefined) {
url.searchParams.append('actors', actors.join(','));
}
if (enableAddingActors !== undefined) {
url.searchParams.append('enableAddingActors', enableAddingActors.toString());
}
if (tools && tools.length > 0) {
if (tools !== undefined) {
url.searchParams.append('tools', tools.join(','));
}

Expand Down Expand Up @@ -61,13 +61,13 @@ export async function createMcpStreamableClient(
}
const url = new URL(serverUrl);
const { actors, enableAddingActors, tools } = options || {};
if (actors) {
if (actors !== undefined) {
url.searchParams.append('actors', actors.join(','));
}
if (enableAddingActors !== undefined) {
url.searchParams.append('enableAddingActors', enableAddingActors.toString());
}
if (tools && tools.length > 0) {
if (tools !== undefined) {
url.searchParams.append('tools', tools.join(','));
}

Expand Down Expand Up @@ -99,13 +99,13 @@ export async function createMcpStdioClient(
}
const { actors, enableAddingActors, tools } = options || {};
const args = ['dist/stdio.js'];
if (actors) {
if (actors !== undefined) {
args.push('--actors', actors.join(','));
}
if (enableAddingActors !== undefined) {
args.push('--enable-adding-actors', enableAddingActors.toString());
}
if (tools && tools.length > 0) {
if (tools !== undefined) {
args.push('--tools', tools.join(','));
}

Expand Down
Loading