From 3f27a38b96cd02394f2b458b99787c2354a230a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kopeck=C3=BD?= Date: Tue, 1 Apr 2025 16:26:42 +0200 Subject: [PATCH 01/22] docs: document input schema processing (#58) document Actor input schema processing in README --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4c73585c..75609ffe 100644 --- a/README.md +++ b/README.md @@ -365,8 +365,13 @@ Upon launching, the Inspector will display a URL that you can access in your bro ## ⓘ Limitations and feedback -To limit the context size the properties in the `input schema` are pruned and description is truncated to 500 characters. -Enum fields and titles are truncated to max 50 options. +The Actor input schema is processed to be compatible with most MCP clients while adhering to [JSON Schema](https://json-schema.org/) standards. The processing includes: +- **Descriptions** are truncated to 500 characters (as defined in `MAX_DESCRIPTION_LENGTH`). +- **Enum fields** are truncated to a maximum combined length of 200 characters for all elements (as defined in `ACTOR_ENUM_MAX_LENGTH`). +- **Required fields** are explicitly marked with a "REQUIRED" prefix in their descriptions for compatibility with frameworks that may not handle JSON schema properly. +- **Nested properties** are built for special cases like proxy configuration and request list sources to ensure correct input structure. +- **Array item types** are inferred when not explicitly defined in the schema, using a priority order: explicit type in items > prefill type > default value type > editor type. +- **Enum values and examples** are added to property descriptions to ensure visibility even if the client doesn't fully support JSON schema. Memory for each Actor is limited to 4GB. Free users have an 8GB limit, 128MB needs to be allocated for running `Actors-MCP-Server`. From 5dd679c8802980aba717b999627a306a4c66831f Mon Sep 17 00:00:00 2001 From: Marc Baiza <43151891+mbaiza27@users.noreply.github.com> Date: Wed, 9 Apr 2025 23:45:59 -0700 Subject: [PATCH 02/22] add VS Code instructions to README (#62) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add vs code instructions --------- Co-authored-by: mbaiza27 Co-authored-by: Jiří Spilka --- README.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/README.md b/README.md index 75609ffe..80ebe749 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ For example it can: To interact with the Apify MCP server, you can use MCP clients such as: - [Claude Desktop](https://claude.ai/download) (only Stdio support) +- [Visual Studio Code](https://code.visualstudio.com/) (Stdio and SSE support) - [LibreChat](https://www.librechat.ai/) (stdio and SSE support (yet without Authorization header)) - [Apify Tester MCP Client](https://apify.com/jiri.spilka/tester-mcp-client) (SSE support with Authorization headers) - other clients at [https://modelcontextprotocol.io/clients](https://modelcontextprotocol.io/clients) @@ -275,6 +276,63 @@ To configure Claude Desktop to work with the MCP server, follow these steps. For Find and analyze instagram profile of the Rock. ``` +#### VS Code + +For one-click installation, click one of the install buttons below: + +[![Install with NPX in VS Code](https://img.shields.io/badge/VS_Code-NPM-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=actors-mcp-server&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40apify%2Factors-mcp-server%22%5D%2C%22env%22%3A%7B%22APIFY_TOKEN%22%3A%22%24%7Binput%3Aapify_token%7D%22%7D%7D&inputs=%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22apify_token%22%2C%22description%22%3A%22Apify+API+Token%22%2C%22password%22%3Atrue%7D%5D) [![Install with NPX in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-NPM-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=actors-mcp-server&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40apify%2Factors-mcp-server%22%5D%2C%22env%22%3A%7B%22APIFY_TOKEN%22%3A%22%24%7Binput%3Aapify_token%7D%22%7D%7D&inputs=%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22apify_token%22%2C%22description%22%3A%22Apify+API+Token%22%2C%22password%22%3Atrue%7D%5D&quality=insiders) + +##### Manual installation + +You can manually install the Apify MCP Server in VS Code. First, click one of the install buttons at the top of this section for a one-click installation. + +Alternatively, add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`. + +```json +{ + "mcp": { + "inputs": [ + { + "type": "promptString", + "id": "apify_token", + "description": "Apify API Token", + "password": true + } + ], + "servers": { + "actors-mcp-server": { + "command": "npx", + "args": ["-y", "@apify/actors-mcp-server"], + "env": { + "APIFY_TOKEN": "${input:apify_token}" + } + } + } + } +} +``` + +Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace - just omit the top-level `mcp {}` key. This will allow you to share the configuration with others. + +If you want to specify which Actors to load, you can add the `--actors` argument: + +```json +{ + "servers": { + "actors-mcp-server": { + "command": "npx", + "args": [ + "-y", "@apify/actors-mcp-server", + "--actors", "lukaskrivka/google-maps-with-contact-details,apify/instagram-scraper" + ], + "env": { + "APIFY_TOKEN": "${input:apify_token}" + } + } + } +} +``` + #### Debugging NPM package @apify/actors-mcp-server with @modelcontextprotocol/inspector To debug the server, use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) tool: From 9a0846b7e14eae808c71f4af8e72f633c8f93d9b Mon Sep 17 00:00:00 2001 From: MQ Date: Mon, 14 Apr 2025 15:13:49 +0200 Subject: [PATCH 03/22] initial working draft of proxy actorized mcp servers --- src/actor/server.ts | 9 ++--- src/actor/types.ts | 7 ---- src/actor/utils.ts | 29 +---------------- src/index.ts | 2 +- src/{actor => }/input.ts | 0 src/main.ts | 6 ++-- src/mcp/actors.ts | 13 ++++++++ src/mcp/client.ts | 41 +++++++++++++++++++++++ src/mcp/const.ts | 3 ++ src/{ => mcp}/mcp-server.ts | 46 +++++++++++++++++++------- src/mcp/proxy.ts | 39 ++++++++++++++++++++++ src/mcp/utils.ts | 65 +++++++++++++++++++++++++++++++++++++ src/stdio.ts | 4 +-- src/tools/actor.ts | 57 +++++++++++++++++++++++++++++++- src/tools/helpers.ts | 4 +-- src/types.ts | 27 +++++++++++++-- 16 files changed, 291 insertions(+), 61 deletions(-) rename src/{actor => }/input.ts (100%) create mode 100644 src/mcp/actors.ts create mode 100644 src/mcp/client.ts create mode 100644 src/mcp/const.ts rename src/{ => mcp}/mcp-server.ts (80%) create mode 100644 src/mcp/proxy.ts create mode 100644 src/mcp/utils.ts diff --git a/src/actor/server.ts b/src/actor/server.ts index 85563ff5..aa493cd1 100644 --- a/src/actor/server.ts +++ b/src/actor/server.ts @@ -9,8 +9,9 @@ import express from 'express'; import log from '@apify/log'; import { HEADER_READINESS_PROBE, Routes } from './const.js'; -import { type ActorsMcpServer } from '../mcp-server.js'; -import { getActorRunData, processParamsGetTools } from './utils.js'; +import { type ActorsMcpServer } from '../mcp/mcp-server.js'; +import { getActorRunData } from './utils.js'; +import { processParamsGetTools } from '../mcp/utils.js'; export function createExpressApp( host: string, @@ -32,7 +33,7 @@ export function createExpressApp( } try { log.info(`Received GET message at: ${Routes.ROOT}`); - const tools = await processParamsGetTools(req.url); + const tools = await processParamsGetTools(req.url, process.env.APIFY_TOKEN as string); if (tools) { mcpServer.updateTools(tools); } @@ -53,7 +54,7 @@ export function createExpressApp( .get(async (req: Request, res: Response) => { try { log.info(`Received GET message at: ${Routes.SSE}`); - const tools = await processParamsGetTools(req.url); + const tools = await processParamsGetTools(req.url, process.env.APIFY_TOKEN as string); if (tools) { mcpServer.updateTools(tools); } diff --git a/src/actor/types.ts b/src/actor/types.ts index 5e9d3fc8..cf953a79 100644 --- a/src/actor/types.ts +++ b/src/actor/types.ts @@ -1,10 +1,3 @@ -export type Input = { - actors: string[] | string; - enableActorAutoLoading?: boolean; - maxActorMemoryBytes?: number; - debugActor?: string; - debugActorInput?: unknown; -}; export interface ActorRunData { id?: string; diff --git a/src/actor/utils.ts b/src/actor/utils.ts index 2a826e9e..e31e91fc 100644 --- a/src/actor/utils.ts +++ b/src/actor/utils.ts @@ -1,34 +1,7 @@ -import { parse } from 'node:querystring'; import { Actor } from 'apify'; +import type { ActorRunData } from './types.js'; -import { processInput } from './input.js'; -import type { ActorRunData, Input } from './types.js'; -import { addTool, getActorsAsTools, removeTool } from '../tools/index.js'; -import type { ToolWrap } from '../types.js'; - -export function parseInputParamsFromUrl(url: string): Input { - const query = url.split('?')[1] || ''; - const params = parse(query) as unknown as Input; - return processInput(params); -} - -/** - * Process input parameters and get tools - * If URL contains query parameter `actors`, return tools from Actors otherwise return null. - * @param url - */ -export async function processParamsGetTools(url: string) { - const input = parseInputParamsFromUrl(url); - let tools: ToolWrap[] = []; - if (input.actors) { - tools = await getActorsAsTools(input.actors as string[]); - } - if (input.enableActorAutoLoading) { - tools.push(addTool, removeTool); - } - return tools; -} export function getActorRunData(): ActorRunData | null { return Actor.isAtHome() ? { diff --git a/src/index.ts b/src/index.ts index e20c084d..bd52f2a6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,5 +3,5 @@ The ActorsMcpServer should be the only class exported from the package */ -import { ActorsMcpServer } from './mcp-server.js'; +import { ActorsMcpServer } from './mcp/mcp-server.js'; export default ActorsMcpServer; diff --git a/src/actor/input.ts b/src/input.ts similarity index 100% rename from src/actor/input.ts rename to src/input.ts diff --git a/src/main.ts b/src/main.ts index 8f70bc3f..5cc3c43c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,10 +8,10 @@ import type { ActorCallOptions } from 'apify-client'; import log from '@apify/log'; -import { processInput } from './actor/input.js'; +import { processInput } from './input.js'; import { createExpressApp } from './actor/server.js'; -import type { Input } from './actor/types'; -import { ActorsMcpServer } from './mcp-server.js'; +import type { Input } from './types.js'; +import { ActorsMcpServer } from './mcp/mcp-server.js'; import { actorDefinitionTool, addTool, removeTool, searchTool, callActorGetDataset } from './tools/index.js'; const STANDBY_MODE = Actor.getEnv().metaOrigin === 'STANDBY'; diff --git a/src/mcp/actors.ts b/src/mcp/actors.ts new file mode 100644 index 00000000..e8c56a93 --- /dev/null +++ b/src/mcp/actors.ts @@ -0,0 +1,13 @@ + +export async function isActorMCPServer(actorID: string): Promise { + // TODO: implement the logic + return actorID === 'apify/actors-mcp-server'; +} + +export async function getActorsMCPServerURL(actorID: string): Promise { + // TODO: implement the logic + if (actorID === 'apify/actors-mcp-server') { + return 'https://actors-mcp-server.apify.actor/sse'; + } + throw new Error(`Actor ${actorID} is not an MCP server`); +} diff --git a/src/mcp/client.ts b/src/mcp/client.ts new file mode 100644 index 00000000..0ede8bb2 --- /dev/null +++ b/src/mcp/client.ts @@ -0,0 +1,41 @@ + +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; +import { getMCPServerID } from "./utils"; + +/** + * Creates and connects a ModelContextProtocol client. + */ +export async function createMCPClient( + url: string, token: string +): Promise { + const transport = new SSEClientTransport( + new URL(url), + { + requestInit: { + headers: { + authorization: `Bearer ${token}`, + }, + }, + eventSourceInit: { + // The EventSource package augments EventSourceInit with a "fetch" parameter. + // You can use this to set additional headers on the outgoing request. + // Based on this example: https://github.com/modelcontextprotocol/typescript-sdk/issues/118 + async fetch(input: Request | URL | string, init?: RequestInit) { + const headers = new Headers(init?.headers || {}); + headers.set('authorization', `Bearer ${token}`); + return fetch(input, { ...init, headers }); + }, + // We have to cast to "any" to use it, since it's non-standard + } as any, // eslint-disable-line @typescript-eslint/no-explicit-any + }); + + const client = new Client({ + name: getMCPServerID(url), + version: "1.0.0", + }); + + await client.connect(transport); + + return client; +} diff --git a/src/mcp/const.ts b/src/mcp/const.ts new file mode 100644 index 00000000..fc57571f --- /dev/null +++ b/src/mcp/const.ts @@ -0,0 +1,3 @@ + +export const MAX_TOOL_NAME_LENGTH = 64; +export const SERVER_ID_LENGTH = 16 diff --git a/src/mcp-server.ts b/src/mcp/mcp-server.ts similarity index 80% rename from src/mcp-server.ts rename to src/mcp/mcp-server.ts index bb54b37a..c8ae6197 100644 --- a/src/mcp-server.ts +++ b/src/mcp/mcp-server.ts @@ -1,3 +1,4 @@ + /** * Model Context Protocol (MCP) server for Apify Actors */ @@ -14,12 +15,14 @@ import { ACTOR_OUTPUT_TRUNCATED_MESSAGE, SERVER_NAME, SERVER_VERSION, -} from './const.js'; -import { actorDefinitionTool, callActorGetDataset, getActorsAsTools, searchTool } from './tools/index.js'; -import type { ActorTool, HelperTool, ToolWrap } from './types.js'; -import { defaults } from './const.js'; -import { actorNameToToolName } from './tools/utils.js'; -import { processParamsGetTools } from './actor/utils.js'; +} from '../const.js'; +import { actorDefinitionTool, callActorGetDataset, getActorsAsTools, searchTool } from '../tools/index.js'; +import type { ActorMCPTool, ActorTool, HelperTool, ToolWrap } from '../types.js'; +import { defaults } from '../const.js'; +import { actorNameToToolName } from '../tools/utils.js'; +import { processParamsGetTools } from './utils.js'; +import { createMCPClient } from './client.js'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; /** * Create Apify MCP server @@ -51,9 +54,9 @@ export class ActorsMcpServer { /** * Loads missing default tools. */ - public async loadDefaultTools() { + public async loadDefaultTools(apifyToken: string) { const missingDefaultTools = defaults.actors.filter(name => !this.tools.has(actorNameToToolName(name))); - const tools = await getActorsAsTools(missingDefaultTools); + const tools = await getActorsAsTools(missingDefaultTools, apifyToken); if (tools.length > 0) this.updateTools(tools); } @@ -62,8 +65,8 @@ export class ActorsMcpServer { * * Used primarily for SSE. */ - public async loadToolsFromUrl(url: string) { - const tools = await processParamsGetTools(url); + public async loadToolsFromUrl(url: string, apifyToken: string) { + const tools = await processParamsGetTools(url, apifyToken); if (tools.length > 0) this.updateTools(tools); } @@ -117,7 +120,10 @@ export class ActorsMcpServer { */ this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; - const apifyToken = request.params.apifyToken || process.env.APIFY_TOKEN; + const apifyToken = (request.params.apifyToken || process.env.APIFY_TOKEN) as string; + + // Remove apifyToken from request.params just in case + delete request.params.apifyToken; // Validate token if (!apifyToken) { @@ -147,11 +153,29 @@ export class ActorsMcpServer { args, apifyMcpServer: this, mcpServer: this.server, + apifyToken, }) as object; return { ...res }; } + if (tool.type === 'actor-mcp') { + const serverTool = tool.tool as ActorMCPTool; + let client: Client | undefined; + try { + client = await createMCPClient(serverTool.serverUrl, apifyToken); + const res = await client.callTool({ + name: name, + arguments: args, + }); + + return { ...res }; + } finally { + if (client) await client.close(); + } + + } + // Handle actor tool if (tool.type === 'actor') { const actorTool = tool.tool as ActorTool; diff --git a/src/mcp/proxy.ts b/src/mcp/proxy.ts new file mode 100644 index 00000000..bdcbd1b8 --- /dev/null +++ b/src/mcp/proxy.ts @@ -0,0 +1,39 @@ +import Ajv from "ajv"; +import { ActorMCPTool, ToolWrap } from "../types"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { getMCPServerID } from "./utils"; + +export async function getMCPServerTools( + actorID: string, + client: Client, + // Name of the MCP server + serverUrl: string +): Promise { + const res = await client.listTools(); + const tools = res.tools; + + + const ajv = new Ajv({ coerceTypes: 'array', strict: false }); + + const compiledTools: ToolWrap[] = []; + for (const tool of tools) { + const mcpTool: ActorMCPTool = { + actorID, + serverId: getMCPServerID(serverUrl), + serverUrl, + name: tool.name, + description: tool.description || "", + inputSchema: tool.inputSchema, + ajvValidate: ajv.compile(tool.inputSchema) + } + + const wrap: ToolWrap = { + type: 'actor-mcp', + tool: mcpTool, + } + + compiledTools.push(wrap); + } + + return compiledTools; +} diff --git a/src/mcp/utils.ts b/src/mcp/utils.ts new file mode 100644 index 00000000..7c82fd43 --- /dev/null +++ b/src/mcp/utils.ts @@ -0,0 +1,65 @@ + +import { createHash } from "node:crypto"; +import { MAX_TOOL_NAME_LENGTH, SERVER_ID_LENGTH } from "./const"; + +import { parse } from 'node:querystring'; +import { processInput } from '../input.js'; +import type { ToolWrap } from '../types.js'; + +import { addTool, getActorsAsTools, removeTool } from '../tools/index.js'; +import { Input } from "../types.js"; +import { getActorsMCPServerURL, isActorMCPServer } from "./actors"; +import { getMCPServerTools } from "./proxy"; +import { createMCPClient } from "./client"; + +/** + * Generates a unique server ID based on the provided URL. + * + * URL is used instead of Actor ID becase one Actor may expose multiple servers - legacy SSE / streamable HTTP. + * + * @param url The URL to generate the server ID from. + * @returns A unique server ID. + */ +export function getMCPServerID(url: string): string { + const serverHashDigest = createHash('sha256').update(url).digest('hex'); + + return serverHashDigest.slice(0, SERVER_ID_LENGTH); +} + +/** + * Generates a unique tool name based on the provided URL and tool name. + * @param url The URL to generate the tool name from. + * @param toolName The tool name to generate the tool name from. + * @returns A unique tool name. + */ +export function getServerToolName(url: string, toolName: string): string { + const prefix = getMCPServerID(url); + + const fullName = `${prefix}-${toolName}`; + return fullName.slice(0, MAX_TOOL_NAME_LENGTH); +} + +/** + * Process input parameters and get tools + * If URL contains query parameter `actors`, return tools from Actors otherwise return null. + * @param url + */ +export async function processParamsGetTools(url: string, apifyToken: string) { + const input = parseInputParamsFromUrl(url); + let tools: ToolWrap[] = []; + if (input.actors) { + const actors = input.actors as string[]; + // Normal Actors as a tool + tools = await getActorsAsTools(actors, apifyToken); + } + if (input.enableActorAutoLoading) { + tools.push(addTool, removeTool); + } + return tools; +} + +export function parseInputParamsFromUrl(url: string): Input { + const query = url.split('?')[1] || ''; + const params = parse(query) as unknown as Input; + return processInput(params); +} diff --git a/src/stdio.ts b/src/stdio.ts index d04525a7..3797fec8 100644 --- a/src/stdio.ts +++ b/src/stdio.ts @@ -18,7 +18,7 @@ import minimist from 'minimist'; import log from '@apify/log'; import { defaults } from './const.js'; -import { ActorsMcpServer } from './mcp-server.js'; +import { ActorsMcpServer } from './mcp/mcp-server.js'; import { addTool, removeTool, getActorsAsTools } from './tools/index.js'; // Configure logging, set to ERROR @@ -37,7 +37,7 @@ if (!process.env.APIFY_TOKEN) { async function main() { const mcpServer = new ActorsMcpServer(); // Initialize tools - const tools = await getActorsAsTools(actorList.length ? actorList : defaults.actors); + const tools = await getActorsAsTools(actorList.length ? actorList : defaults.actors, process.env.APIFY_TOKEN as string); if (enableActorAutoLoading) { tools.push(addTool, removeTool); } diff --git a/src/tools/actor.ts b/src/tools/actor.ts index 45af401f..558de9b5 100644 --- a/src/tools/actor.ts +++ b/src/tools/actor.ts @@ -15,6 +15,11 @@ import { markInputPropertiesAsRequired, shortenProperties, } from './utils.js'; +import { getActorsMCPServerURL, isActorMCPServer } from '../mcp/actors.js'; +import { createMCPClient } from '../mcp/client.js'; +import { getMCPServerTools } from '../mcp/proxy.js'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { cp } from 'node:fs'; /** * Calls an Apify actor and retrieves the dataset items. @@ -55,6 +60,8 @@ export async function callActorGetDataset( } /** + * This function is used to fetch normal non-MCP server Actors as a tool. + * * Fetches actor input schemas by Actor IDs or Actor full names and creates MCP tools. * * This function retrieves the input schemas for the specified actors and compiles them into MCP tools. @@ -72,7 +79,7 @@ export async function callActorGetDataset( * @param {string[]} actors - An array of actor IDs or Actor full names. * @returns {Promise} - A promise that resolves to an array of MCP tools. */ -export async function getActorsAsTools(actors: string[]): Promise { +export async function getNormalActorsAsTools(actors: string[]): Promise { const ajv = new Ajv({ coerceTypes: 'array', strict: false }); const results = await Promise.all(actors.map(getActorDefinition)); const tools: ToolWrap[] = []; @@ -105,3 +112,51 @@ export async function getActorsAsTools(actors: string[]): Promise { } return tools; } + +async function getMCPServersAsTools( + actors: string[], + apifyToken: string +): Promise { + const actorsMCPServerTools: ToolWrap[] = []; + for (const actorID of actors) { + const serverUrl = await getActorsMCPServerURL(actorID); + + let client: Client | undefined; + try { + client = await createMCPClient(serverUrl, apifyToken); + const serverTools = await getMCPServerTools(actorID, client, serverUrl) + actorsMCPServerTools.push(...serverTools); + } finally { + if (client) await client.close(); + } + } + + return actorsMCPServerTools; +} + +export async function getActorsAsTools( + actors: string[], + apifyToken: string +): Promise { + console.log('Fetching actors as tools...'); + console.log(actors) + // Actorized MCP servers + const actorsMCPServer: string[] = []; + for (const actorID of actors) { + if (await isActorMCPServer(actorID)) { + actorsMCPServer.push(actorID); + } + } + // Normal Actors as a tool + const toolActors = actors.filter((actorID) => !actorsMCPServer.includes(actorID)); + console.log('actorsMCPserver', actorsMCPServer); + console.log('toolActors', toolActors); + + // Normal Actors as a tool + const normalTools = await getNormalActorsAsTools(toolActors); + + // Tools from Actorized MCP servers + const mcpServerTools = await getMCPServersAsTools(actorsMCPServer, apifyToken); + + return [...normalTools, ...mcpServerTools]; +} diff --git a/src/tools/helpers.ts b/src/tools/helpers.ts index fbe18af4..56476889 100644 --- a/src/tools/helpers.ts +++ b/src/tools/helpers.ts @@ -25,9 +25,9 @@ export const addTool: ToolWrap = { ajvValidate: ajv.compile(zodToJsonSchema(AddToolArgsSchema)), // TODO: I don't like that we are passing apifyMcpServer and mcpServer to the tool call: async (toolArgs) => { - const { apifyMcpServer, mcpServer, args } = toolArgs; + const { apifyMcpServer, mcpServer, apifyToken, args } = toolArgs; const parsed = AddToolArgsSchema.parse(args); - const tools = await getActorsAsTools([parsed.actorName]); + const tools = await getActorsAsTools([parsed.actorName], apifyToken); const toolsAdded = apifyMcpServer.updateTools(tools); await mcpServer.notification({ method: 'notifications/tools/list_changed' }); diff --git a/src/types.ts b/src/types.ts index dc8da045..9b62b582 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,8 +1,9 @@ + import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; import type { ValidateFunction } from 'ajv'; import type { ActorDefaultRunOptions, ActorDefinition } from 'apify-client'; -import type { ActorsMcpServer } from './mcp-server.js'; +import type { ActorsMcpServer } from './mcp/mcp-server.js'; export interface ISchemaProperties { type: string; @@ -83,6 +84,8 @@ export type InternalToolArgs = { apifyMcpServer: ActorsMcpServer; /** Reference to the MCP server instance */ mcpServer: Server; + /** Apify API token */ + apifyToken: string; } /** @@ -98,6 +101,18 @@ export interface HelperTool extends ToolBase { call: (toolArgs: InternalToolArgs) => Promise; } +/** +* Actorized MCP server tool where this MCP server acts as a proxy. +* Extends ToolBase with tool associated MCP server. +*/ +export interface ActorMCPTool extends ToolBase { + // ID of the Actorized MCP server + actorID: string; + // Name of the Actorized MCP server the tool is associated with + serverId: string; + serverUrl: string; +} + /** * Type discriminator for tools - indicates whether a tool is internal or Actor-based. */ @@ -111,7 +126,7 @@ export interface ToolWrap { /** Type of the tool (internal or actor) */ type: ToolType; /** The tool instance */ - tool: ActorTool | HelperTool; + tool: ActorTool | HelperTool | ActorMCPTool; } // ActorStoreList for actor-search tool @@ -152,3 +167,11 @@ export interface InternalTool extends ToolBase { */ call: (toolArgs: InternalToolArgs) => Promise; } + +export type Input = { + actors: string[] | string; + enableActorAutoLoading?: boolean; + maxActorMemoryBytes?: number; + debugActor?: string; + debugActorInput?: unknown; +}; From b8b3ce384c5d29c6f649d3c85c127244a82c026f Mon Sep 17 00:00:00 2001 From: MQ Date: Mon, 14 Apr 2025 15:28:13 +0200 Subject: [PATCH 04/22] fix tests --- tests/actor-server-test.ts | 2 +- tests/actor-utils-test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/actor-server-test.ts b/tests/actor-server-test.ts index 2a1d16ee..351c070e 100644 --- a/tests/actor-server-test.ts +++ b/tests/actor-server-test.ts @@ -7,7 +7,7 @@ import log from '@apify/log'; import { createExpressApp } from '../src/actor/server.js'; import { HelperTools } from '../src/const.js'; -import { ActorsMcpServer } from '../src/mcp-server.js'; +import { ActorsMcpServer } from '../src/mcp/mcp-server.js'; describe('ApifyMcpServer initialization', () => { let app: Express; diff --git a/tests/actor-utils-test.ts b/tests/actor-utils-test.ts index fa77b62f..931731cd 100644 --- a/tests/actor-utils-test.ts +++ b/tests/actor-utils-test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { parseInputParamsFromUrl } from '../src/actor/utils.js'; +import { parseInputParamsFromUrl } from '../src/mcp/utils.js'; describe('parseInputParamsFromUrl', () => { it('should parse actors from URL query params', () => { From 3543a5ed12d18906cb8eb43370c8bb973e9fdeae Mon Sep 17 00:00:00 2001 From: MQ Date: Mon, 14 Apr 2025 15:48:27 +0200 Subject: [PATCH 05/22] fix proxy tool name handling --- src/mcp/const.ts | 2 +- src/mcp/mcp-server.ts | 4 +++- src/mcp/proxy.ts | 6 ++++-- src/mcp/utils.ts | 5 +---- src/types.ts | 8 +++++++- 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/mcp/const.ts b/src/mcp/const.ts index fc57571f..ffbd88cd 100644 --- a/src/mcp/const.ts +++ b/src/mcp/const.ts @@ -1,3 +1,3 @@ export const MAX_TOOL_NAME_LENGTH = 64; -export const SERVER_ID_LENGTH = 16 +export const SERVER_ID_LENGTH = 8 diff --git a/src/mcp/mcp-server.ts b/src/mcp/mcp-server.ts index c8ae6197..eb029340 100644 --- a/src/mcp/mcp-server.ts +++ b/src/mcp/mcp-server.ts @@ -63,6 +63,8 @@ export class ActorsMcpServer { /** * Loads tools from URL params. * + * This method also handles enabling of Actor auto loading via the processParamsGetTools. + * * Used primarily for SSE. */ public async loadToolsFromUrl(url: string, apifyToken: string) { @@ -165,7 +167,7 @@ export class ActorsMcpServer { try { client = await createMCPClient(serverTool.serverUrl, apifyToken); const res = await client.callTool({ - name: name, + name: serverTool.originToolName, arguments: args, }); diff --git a/src/mcp/proxy.ts b/src/mcp/proxy.ts index bdcbd1b8..a9c8286c 100644 --- a/src/mcp/proxy.ts +++ b/src/mcp/proxy.ts @@ -1,7 +1,7 @@ import Ajv from "ajv"; import { ActorMCPTool, ToolWrap } from "../types"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { getMCPServerID } from "./utils"; +import { getMCPServerID, getProxyMCPServerToolName } from "./utils"; export async function getMCPServerTools( actorID: string, @@ -21,7 +21,9 @@ export async function getMCPServerTools( actorID, serverId: getMCPServerID(serverUrl), serverUrl, - name: tool.name, + originToolName: tool.name, + + name: getProxyMCPServerToolName(serverUrl, tool.name), description: tool.description || "", inputSchema: tool.inputSchema, ajvValidate: ajv.compile(tool.inputSchema) diff --git a/src/mcp/utils.ts b/src/mcp/utils.ts index 7c82fd43..dfaf184d 100644 --- a/src/mcp/utils.ts +++ b/src/mcp/utils.ts @@ -8,9 +8,6 @@ import type { ToolWrap } from '../types.js'; import { addTool, getActorsAsTools, removeTool } from '../tools/index.js'; import { Input } from "../types.js"; -import { getActorsMCPServerURL, isActorMCPServer } from "./actors"; -import { getMCPServerTools } from "./proxy"; -import { createMCPClient } from "./client"; /** * Generates a unique server ID based on the provided URL. @@ -32,7 +29,7 @@ export function getMCPServerID(url: string): string { * @param toolName The tool name to generate the tool name from. * @returns A unique tool name. */ -export function getServerToolName(url: string, toolName: string): string { +export function getProxyMCPServerToolName(url: string, toolName: string): string { const prefix = getMCPServerID(url); const fullName = `${prefix}-${toolName}`; diff --git a/src/types.ts b/src/types.ts index 9b62b582..db3e08c4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -106,10 +106,16 @@ export interface HelperTool extends ToolBase { * Extends ToolBase with tool associated MCP server. */ export interface ActorMCPTool extends ToolBase { + // Origin MCP server tool name, is needed for the tool call + originToolName: string; // ID of the Actorized MCP server actorID: string; - // Name of the Actorized MCP server the tool is associated with + /** + * ID of the Actorized MCP server the tool is associated with. + * See getMCPServerID() + */ serverId: string; + // Connection URL of the Actorized MCP server serverUrl: string; } From 6c488ddcc89bdb8206f99d469784cf1587bf1884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kopeck=C3=BD?= Date: Mon, 14 Apr 2025 22:38:58 +0200 Subject: [PATCH 06/22] ci: run prerelase manually (#70) run prerelase manually --- .github/workflows/pre_release.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pre_release.yaml b/.github/workflows/pre_release.yaml index 321ab834..7b0e4300 100644 --- a/.github/workflows/pre_release.yaml +++ b/.github/workflows/pre_release.yaml @@ -1,6 +1,7 @@ name: Create a pre-release on: + workflow_dispatch: # Push to master will deploy a beta version push: branches: From c273cfb020530d6b7c14063da28eb3cea47efb5c Mon Sep 17 00:00:00 2001 From: MQ Date: Tue, 15 Apr 2025 10:08:28 +0200 Subject: [PATCH 07/22] organize, fix passing of apify token --- src/actor/server.ts | 2 +- .../mcp-apify-client.ts => apify-client.ts} | 2 +- src/index.ts | 3 ++- src/main.ts | 3 ++- src/mcp/{mcp-server.ts => server.ts} | 0 src/stdio.ts | 2 +- src/tools/actor.ts | 15 +++++++++++---- src/tools/build.ts | 14 +++++++++----- src/tools/store_collection.ts | 8 +++++--- src/types.ts | 2 +- tests/actor-server-test.ts | 2 +- 11 files changed, 34 insertions(+), 19 deletions(-) rename src/{tools/mcp-apify-client.ts => apify-client.ts} (94%) rename src/mcp/{mcp-server.ts => server.ts} (100%) diff --git a/src/actor/server.ts b/src/actor/server.ts index aa493cd1..5bda555d 100644 --- a/src/actor/server.ts +++ b/src/actor/server.ts @@ -9,7 +9,7 @@ import express from 'express'; import log from '@apify/log'; import { HEADER_READINESS_PROBE, Routes } from './const.js'; -import { type ActorsMcpServer } from '../mcp/mcp-server.js'; +import { type ActorsMcpServer } from '../mcp/server.js'; import { getActorRunData } from './utils.js'; import { processParamsGetTools } from '../mcp/utils.js'; diff --git a/src/tools/mcp-apify-client.ts b/src/apify-client.ts similarity index 94% rename from src/tools/mcp-apify-client.ts rename to src/apify-client.ts index 0d3dd12b..5ed37bac 100644 --- a/src/tools/mcp-apify-client.ts +++ b/src/apify-client.ts @@ -3,7 +3,7 @@ import type { ApifyClientOptions } from 'apify'; import { ApifyClient as _ApifyClient } from 'apify-client'; import type { AxiosRequestConfig } from 'axios'; -import { USER_AGENT_ORIGIN } from '../const.js'; +import { USER_AGENT_ORIGIN } from './const.js'; /** * Adds a User-Agent header to the request config. diff --git a/src/index.ts b/src/index.ts index bd52f2a6..acf80e9f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,8 @@ + /* This file provides essential functions and tools for MCP servers, serving as a library. The ActorsMcpServer should be the only class exported from the package */ -import { ActorsMcpServer } from './mcp/mcp-server.js'; +import { ActorsMcpServer } from './mcp/server.js'; export default ActorsMcpServer; diff --git a/src/main.ts b/src/main.ts index 5cc3c43c..5fecbfdc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,3 +1,4 @@ + /** * Serves as an Actor MCP SSE server entry point. * This file needs to be named `main.ts` to be recognized by the Apify platform. @@ -11,7 +12,7 @@ import log from '@apify/log'; import { processInput } from './input.js'; import { createExpressApp } from './actor/server.js'; import type { Input } from './types.js'; -import { ActorsMcpServer } from './mcp/mcp-server.js'; +import { ActorsMcpServer } from './mcp/server.js'; import { actorDefinitionTool, addTool, removeTool, searchTool, callActorGetDataset } from './tools/index.js'; const STANDBY_MODE = Actor.getEnv().metaOrigin === 'STANDBY'; diff --git a/src/mcp/mcp-server.ts b/src/mcp/server.ts similarity index 100% rename from src/mcp/mcp-server.ts rename to src/mcp/server.ts diff --git a/src/stdio.ts b/src/stdio.ts index 3797fec8..2beff437 100644 --- a/src/stdio.ts +++ b/src/stdio.ts @@ -18,7 +18,7 @@ import minimist from 'minimist'; import log from '@apify/log'; import { defaults } from './const.js'; -import { ActorsMcpServer } from './mcp/mcp-server.js'; +import { ActorsMcpServer } from './mcp/server.js'; import { addTool, removeTool, getActorsAsTools } from './tools/index.js'; // Configure logging, set to ERROR diff --git a/src/tools/actor.ts b/src/tools/actor.ts index 558de9b5..e7cca313 100644 --- a/src/tools/actor.ts +++ b/src/tools/actor.ts @@ -6,7 +6,7 @@ import log from '@apify/log'; import type { ToolWrap } from '../types.js'; import { getActorDefinition } from './build.js'; import { ACTOR_ADDITIONAL_INSTRUCTIONS, ACTOR_MAX_MEMORY_MBYTES } from '../const.js'; -import { ApifyClient } from './mcp-apify-client.js'; +import { ApifyClient } from '../apify-client.js'; import { actorNameToToolName, addEnumsToDescriptionsWithExamples, @@ -79,9 +79,16 @@ export async function callActorGetDataset( * @param {string[]} actors - An array of actor IDs or Actor full names. * @returns {Promise} - A promise that resolves to an array of MCP tools. */ -export async function getNormalActorsAsTools(actors: string[]): Promise { +export async function getNormalActorsAsTools( + actors: string[], + apifyToken: string, +): Promise { const ajv = new Ajv({ coerceTypes: 'array', strict: false }); - const results = await Promise.all(actors.map(getActorDefinition)); + const getActorDefinitionWithToken = async (actorId: string) => { + const actor = await getActorDefinition(actorId, apifyToken); + return actor; + }; + const results = await Promise.all(actors.map(getActorDefinitionWithToken)); const tools: ToolWrap[] = []; for (const result of results) { if (result) { @@ -153,7 +160,7 @@ export async function getActorsAsTools( console.log('toolActors', toolActors); // Normal Actors as a tool - const normalTools = await getNormalActorsAsTools(toolActors); + const normalTools = await getNormalActorsAsTools(toolActors, apifyToken); // Tools from Actorized MCP servers const mcpServerTools = await getMCPServersAsTools(actorsMCPServer, apifyToken); diff --git a/src/tools/build.ts b/src/tools/build.ts index 10c5dfb6..0e4e0f89 100644 --- a/src/tools/build.ts +++ b/src/tools/build.ts @@ -1,5 +1,5 @@ import { Ajv } from 'ajv'; -import { ApifyClient } from 'apify-client'; +import { ApifyClient } from '../apify-client.js'; import { z } from 'zod'; import zodToJsonSchema from 'zod-to-json-schema'; @@ -19,8 +19,12 @@ const ajv = new Ajv({ coerceTypes: 'array', strict: false }); * @param {number} limit - Truncate the README to this limit. * @returns {Promise} - The actor definition with description or null if not found. */ -export async function getActorDefinition(actorIdOrName: string, limit: number = ACTOR_README_MAX_LENGTH): Promise { - const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); +export async function getActorDefinition( + actorIdOrName: string, + apifyToken: string, + limit: number = ACTOR_README_MAX_LENGTH +): Promise { + const client = new ApifyClient({ token: apifyToken }); const actorClient = client.actor(actorIdOrName); try { // Fetch actor details @@ -114,10 +118,10 @@ export const actorDefinitionTool: ToolWrap = { inputSchema: zodToJsonSchema(GetActorDefinitionArgsSchema), ajvValidate: ajv.compile(zodToJsonSchema(GetActorDefinitionArgsSchema)), call: async (toolArgs) => { - const { args } = toolArgs; + const { args, apifyToken } = toolArgs; const parsed = GetActorDefinitionArgsSchema.parse(args); - const v = await getActorDefinition(parsed.actorName, parsed.limit); + const v = await getActorDefinition(parsed.actorName, apifyToken, parsed.limit); if (v && v.input && 'properties' in v.input && v.input) { const properties = filterSchemaProperties(v.input.properties as { [key: string]: ISchemaProperties }); v.input.properties = shortenProperties(properties); diff --git a/src/tools/store_collection.ts b/src/tools/store_collection.ts index 5130624d..6d4999a9 100644 --- a/src/tools/store_collection.ts +++ b/src/tools/store_collection.ts @@ -1,6 +1,6 @@ import { Ajv } from 'ajv'; import type { ActorStoreList } from 'apify-client'; -import { ApifyClient } from 'apify-client'; +import { ApifyClient } from '../apify-client.js'; import { z } from 'zod'; import zodToJsonSchema from 'zod-to-json-schema'; @@ -35,10 +35,11 @@ function pruneActorStoreInfo(response: ActorStoreList): ActorStorePruned { export async function searchActorsByKeywords( search: string, + apifyToken: string, limit: number | undefined = undefined, offset: number | undefined = undefined, ): Promise { - const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); + const client = new ApifyClient({ token: apifyToken }); const results = await client.store().list({ search, limit, offset }); return results.items.map((x) => pruneActorStoreInfo(x)); } @@ -80,10 +81,11 @@ export const searchTool: ToolWrap = { inputSchema: zodToJsonSchema(SearchToolArgsSchema), ajvValidate: ajv.compile(zodToJsonSchema(SearchToolArgsSchema)), call: async (toolArgs) => { - const { args } = toolArgs; + const { args, apifyToken } = toolArgs; const parsed = SearchToolArgsSchema.parse(args); const actors = await searchActorsByKeywords( parsed.search, + apifyToken, parsed.limit, parsed.offset, ); diff --git a/src/types.ts b/src/types.ts index db3e08c4..5b611035 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,7 +3,7 @@ import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; import type { ValidateFunction } from 'ajv'; import type { ActorDefaultRunOptions, ActorDefinition } from 'apify-client'; -import type { ActorsMcpServer } from './mcp/mcp-server.js'; +import type { ActorsMcpServer } from './mcp/server.js'; export interface ISchemaProperties { type: string; diff --git a/tests/actor-server-test.ts b/tests/actor-server-test.ts index 351c070e..f39bc06b 100644 --- a/tests/actor-server-test.ts +++ b/tests/actor-server-test.ts @@ -7,7 +7,7 @@ import log from '@apify/log'; import { createExpressApp } from '../src/actor/server.js'; import { HelperTools } from '../src/const.js'; -import { ActorsMcpServer } from '../src/mcp/mcp-server.js'; +import { ActorsMcpServer } from '../src/mcp/server.js'; describe('ApifyMcpServer initialization', () => { let app: Express; From e005b652a0f7d6b79867460fcaf8d591aa249a0e Mon Sep 17 00:00:00 2001 From: MQ Date: Tue, 15 Apr 2025 11:21:56 +0200 Subject: [PATCH 08/22] fix env var name --- src/apify-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apify-client.ts b/src/apify-client.ts index 5ed37bac..be7ec73c 100644 --- a/src/apify-client.ts +++ b/src/apify-client.ts @@ -21,7 +21,7 @@ export class ApifyClient extends _ApifyClient { constructor(options: ApifyClientOptions) { super({ ...options, - baseUrl: process.env.MCP_APIFY_BASE_URL || undefined, + baseUrl: process.env.APIFY_API_BASE_URL || undefined, requestInterceptors: [addUserAgent], }); } From 0bf2e0c49a8691eeb7e284038bb5c7a6e1985023 Mon Sep 17 00:00:00 2001 From: MQ Date: Tue, 15 Apr 2025 11:39:35 +0200 Subject: [PATCH 09/22] fix imports --- src/mcp/client.ts | 2 +- src/mcp/proxy.ts | 4 ++-- src/mcp/utils.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mcp/client.ts b/src/mcp/client.ts index 0ede8bb2..611a70f5 100644 --- a/src/mcp/client.ts +++ b/src/mcp/client.ts @@ -1,7 +1,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; -import { getMCPServerID } from "./utils"; +import { getMCPServerID } from "./utils.js"; /** * Creates and connects a ModelContextProtocol client. diff --git a/src/mcp/proxy.ts b/src/mcp/proxy.ts index a9c8286c..9870a77c 100644 --- a/src/mcp/proxy.ts +++ b/src/mcp/proxy.ts @@ -1,7 +1,7 @@ import Ajv from "ajv"; -import { ActorMCPTool, ToolWrap } from "../types"; +import { ActorMCPTool, ToolWrap } from "../types.js"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { getMCPServerID, getProxyMCPServerToolName } from "./utils"; +import { getMCPServerID, getProxyMCPServerToolName } from "./utils.js"; export async function getMCPServerTools( actorID: string, diff --git a/src/mcp/utils.ts b/src/mcp/utils.ts index dfaf184d..45cb41df 100644 --- a/src/mcp/utils.ts +++ b/src/mcp/utils.ts @@ -1,6 +1,6 @@ import { createHash } from "node:crypto"; -import { MAX_TOOL_NAME_LENGTH, SERVER_ID_LENGTH } from "./const"; +import { MAX_TOOL_NAME_LENGTH, SERVER_ID_LENGTH } from "./const.js"; import { parse } from 'node:querystring'; import { processInput } from '../input.js'; From f14025db7813360b5989d862ce48e91c62370b30 Mon Sep 17 00:00:00 2001 From: MQ Date: Tue, 15 Apr 2025 13:27:31 +0200 Subject: [PATCH 10/22] get standby url from actor id, check if is mcp server based on actor name for now --- src/const.ts | 2 ++ src/mcp/actors.ts | 16 ++++++++-------- src/mcp/utils.ts | 20 ++++++++++++++++++++ src/tools/actor.ts | 14 +++++++------- 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/const.ts b/src/const.ts index 7758896c..36b5b61e 100644 --- a/src/const.ts +++ b/src/const.ts @@ -37,3 +37,5 @@ export const defaults = { enableActorAutoLoading: false, maxMemoryMbytes: 4096, }; + +export const APIFY_USERNAME = 'apify'; diff --git a/src/mcp/actors.ts b/src/mcp/actors.ts index e8c56a93..82cceb0d 100644 --- a/src/mcp/actors.ts +++ b/src/mcp/actors.ts @@ -1,13 +1,13 @@ -export async function isActorMCPServer(actorID: string): Promise { +import { getActorStandbyURL } from "./utils.js"; + +export async function isActorMCPServer(actorID: string, _apifyToken: string): Promise { // TODO: implement the logic - return actorID === 'apify/actors-mcp-server'; + return actorID.toLowerCase().includes('mcp-') || actorID.toLowerCase().includes('-mcp'); } -export async function getActorsMCPServerURL(actorID: string): Promise { - // TODO: implement the logic - if (actorID === 'apify/actors-mcp-server') { - return 'https://actors-mcp-server.apify.actor/sse'; - } - throw new Error(`Actor ${actorID} is not an MCP server`); +export async function getActorsMCPServerURL(actorID: string, _apifyToken: string): Promise { + // TODO: get from API instead + const standbyUrl = getActorStandbyURL(actorID) + return `${standbyUrl}/sse`; } diff --git a/src/mcp/utils.ts b/src/mcp/utils.ts index 45cb41df..aca535ff 100644 --- a/src/mcp/utils.ts +++ b/src/mcp/utils.ts @@ -8,6 +8,7 @@ import type { ToolWrap } from '../types.js'; import { addTool, getActorsAsTools, removeTool } from '../tools/index.js'; import { Input } from "../types.js"; +import { APIFY_USERNAME } from "../const.js"; /** * Generates a unique server ID based on the provided URL. @@ -60,3 +61,22 @@ export function parseInputParamsFromUrl(url: string): Input { const params = parse(query) as unknown as Input; return processInput(params); } + +/** +* Returns standby URL for given Actor ID. +* +* @param actorID +* @param standbyBaseUrl +* @returns +*/ +export function getActorStandbyURL(actorID: string, standbyBaseUrl = '.apify.actor'): string { + const actorOwner = actorID.split('/')[0]; + const actorName = actorID.split('/')[1]; + if (!actorOwner || !actorName) { + throw new Error(`Invalid actor ID: ${actorID}`); + } + + const prefix = actorOwner === APIFY_USERNAME ? '' : `${actorOwner}--`; + + return `https://${prefix}${actorName}${standbyBaseUrl}`; +} diff --git a/src/tools/actor.ts b/src/tools/actor.ts index e7cca313..56f83f04 100644 --- a/src/tools/actor.ts +++ b/src/tools/actor.ts @@ -126,7 +126,7 @@ async function getMCPServersAsTools( ): Promise { const actorsMCPServerTools: ToolWrap[] = []; for (const actorID of actors) { - const serverUrl = await getActorsMCPServerURL(actorID); + const serverUrl = await getActorsMCPServerURL(actorID, apifyToken); let client: Client | undefined; try { @@ -148,22 +148,22 @@ export async function getActorsAsTools( console.log('Fetching actors as tools...'); console.log(actors) // Actorized MCP servers - const actorsMCPServer: string[] = []; + const actorsMCPServers: string[] = []; for (const actorID of actors) { - if (await isActorMCPServer(actorID)) { - actorsMCPServer.push(actorID); + if (await isActorMCPServer(actorID, apifyToken)) { + actorsMCPServers.push(actorID); } } // Normal Actors as a tool - const toolActors = actors.filter((actorID) => !actorsMCPServer.includes(actorID)); - console.log('actorsMCPserver', actorsMCPServer); + const toolActors = actors.filter((actorID) => !actorsMCPServers.includes(actorID)); + console.log('actorsMCPserver', actorsMCPServers); console.log('toolActors', toolActors); // Normal Actors as a tool const normalTools = await getNormalActorsAsTools(toolActors, apifyToken); // Tools from Actorized MCP servers - const mcpServerTools = await getMCPServersAsTools(actorsMCPServer, apifyToken); + const mcpServerTools = await getMCPServersAsTools(actorsMCPServers, apifyToken); return [...normalTools, ...mcpServerTools]; } From caaebb5e5c2660928e22bc8d96b2996fa201d254 Mon Sep 17 00:00:00 2001 From: MQ Date: Tue, 15 Apr 2025 13:53:26 +0200 Subject: [PATCH 11/22] Actor MCP server load default Actors --- src/main.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 5fecbfdc..95b32595 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,7 +13,8 @@ import { processInput } from './input.js'; import { createExpressApp } from './actor/server.js'; import type { Input } from './types.js'; import { ActorsMcpServer } from './mcp/server.js'; -import { actorDefinitionTool, addTool, removeTool, searchTool, callActorGetDataset } from './tools/index.js'; +import { actorDefinitionTool, addTool, removeTool, searchTool, callActorGetDataset, getActorsAsTools } from './tools/index.js'; +import { defaults } from './const.js'; const STANDBY_MODE = Actor.getEnv().metaOrigin === 'STANDBY'; @@ -39,6 +40,14 @@ if (STANDBY_MODE) { if (input.enableActorAutoLoading) { tools.push(addTool, removeTool); } + if (input.actors === undefined || input.actors === null) { + const actorTools = await getActorsAsTools(defaults.actors, process.env.APIFY_TOKEN as string); + tools.push(...actorTools); + } else { + const actorsToLoad = Array.isArray(input.actors) ? input.actors : input.actors.split(','); + const actorTools = await getActorsAsTools(actorsToLoad, process.env.APIFY_TOKEN as string); + tools.push(...actorTools); + } mcpServer.updateTools(tools); app.listen(PORT, () => { log.info(`The Actor web server is listening for user requests at ${HOST}`); From c2caf6c3dd91a1cf3411fcb7b43f788360de665a Mon Sep 17 00:00:00 2001 From: MQ Date: Tue, 15 Apr 2025 13:58:12 +0200 Subject: [PATCH 12/22] get standbyUrlBase from env var --- src/mcp/actors.ts | 4 +++- src/mcp/utils.ts | 4 ++-- src/tools/actor.ts | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/mcp/actors.ts b/src/mcp/actors.ts index 82cceb0d..b42c34f8 100644 --- a/src/mcp/actors.ts +++ b/src/mcp/actors.ts @@ -8,6 +8,8 @@ export async function isActorMCPServer(actorID: string, _apifyToken: string): Pr export async function getActorsMCPServerURL(actorID: string, _apifyToken: string): Promise { // TODO: get from API instead - const standbyUrl = getActorStandbyURL(actorID) + const standbyBaseUrl = process.env.HOSTNAME === 'mcp-securitybyobscurity.apify.com' ? + '.mcp-securitybyobscurity.apify.actor' : '.apify.actor'; + const standbyUrl = getActorStandbyURL(actorID, standbyBaseUrl); return `${standbyUrl}/sse`; } diff --git a/src/mcp/utils.ts b/src/mcp/utils.ts index aca535ff..2d89c20f 100644 --- a/src/mcp/utils.ts +++ b/src/mcp/utils.ts @@ -69,7 +69,7 @@ export function parseInputParamsFromUrl(url: string): Input { * @param standbyBaseUrl * @returns */ -export function getActorStandbyURL(actorID: string, standbyBaseUrl = '.apify.actor'): string { +export function getActorStandbyURL(actorID: string, standbyBaseUrl = 'apify.actor'): string { const actorOwner = actorID.split('/')[0]; const actorName = actorID.split('/')[1]; if (!actorOwner || !actorName) { @@ -78,5 +78,5 @@ export function getActorStandbyURL(actorID: string, standbyBaseUrl = '.apify.act const prefix = actorOwner === APIFY_USERNAME ? '' : `${actorOwner}--`; - return `https://${prefix}${actorName}${standbyBaseUrl}`; + return `https://${prefix}${actorName}.${standbyBaseUrl}`; } diff --git a/src/tools/actor.ts b/src/tools/actor.ts index 56f83f04..c834f149 100644 --- a/src/tools/actor.ts +++ b/src/tools/actor.ts @@ -127,6 +127,7 @@ async function getMCPServersAsTools( const actorsMCPServerTools: ToolWrap[] = []; for (const actorID of actors) { const serverUrl = await getActorsMCPServerURL(actorID, apifyToken); + log.info(`ActorID: ${actorID} MCP server URL: ${serverUrl}`); let client: Client | undefined; try { From 364c0c4a587c3814d1196d8b285ee9e2eb47b5f7 Mon Sep 17 00:00:00 2001 From: MQ Date: Tue, 15 Apr 2025 14:14:37 +0200 Subject: [PATCH 13/22] fix standby url base, use dns friendly owner name --- src/mcp/actors.ts | 2 +- src/mcp/utils.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/mcp/actors.ts b/src/mcp/actors.ts index b42c34f8..6d51faf3 100644 --- a/src/mcp/actors.ts +++ b/src/mcp/actors.ts @@ -9,7 +9,7 @@ export async function isActorMCPServer(actorID: string, _apifyToken: string): Pr export async function getActorsMCPServerURL(actorID: string, _apifyToken: string): Promise { // TODO: get from API instead const standbyBaseUrl = process.env.HOSTNAME === 'mcp-securitybyobscurity.apify.com' ? - '.mcp-securitybyobscurity.apify.actor' : '.apify.actor'; + 'securitybyobscurity.apify.actor' : 'apify.actor'; const standbyUrl = getActorStandbyURL(actorID, standbyBaseUrl); return `${standbyUrl}/sse`; } diff --git a/src/mcp/utils.ts b/src/mcp/utils.ts index 2d89c20f..f7926c31 100644 --- a/src/mcp/utils.ts +++ b/src/mcp/utils.ts @@ -76,7 +76,8 @@ export function getActorStandbyURL(actorID: string, standbyBaseUrl = 'apify.acto throw new Error(`Invalid actor ID: ${actorID}`); } - const prefix = actorOwner === APIFY_USERNAME ? '' : `${actorOwner}--`; + const actorOwnerDNSFriendly = actorOwner.replace('.', '-'); + const prefix = actorOwner === APIFY_USERNAME ? '' : `${actorOwnerDNSFriendly}--`; return `https://${prefix}${actorName}.${standbyBaseUrl}`; } From 69c4327389083ded452665914200d999304d7168 Mon Sep 17 00:00:00 2001 From: MQ Date: Tue, 15 Apr 2025 15:06:30 +0200 Subject: [PATCH 14/22] get mcp path from definition --- src/mcp/actors.ts | 17 ++++++++++++----- src/mcp/utils.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/mcp/actors.ts b/src/mcp/actors.ts index 6d51faf3..7f85c90a 100644 --- a/src/mcp/actors.ts +++ b/src/mcp/actors.ts @@ -1,9 +1,15 @@ -import { getActorStandbyURL } from "./utils.js"; +import { ActorDefinition } from "apify-client"; +import { getActorDefinition, getActorStandbyURL } from "./utils.js"; -export async function isActorMCPServer(actorID: string, _apifyToken: string): Promise { - // TODO: implement the logic - return actorID.toLowerCase().includes('mcp-') || actorID.toLowerCase().includes('-mcp'); +export async function isActorMCPServer(actorID: string, apifyToken: string): Promise { + const mcpPath = await getActorsMCPServerPath(actorID, apifyToken); + return (mcpPath?.length || 0) > 0; +} + +export async function getActorsMCPServerPath(actorID: string, apifyToken: string): Promise { + const actorDefinition = await getActorDefinition(actorID, apifyToken); + return (actorDefinition as any).webServerMcpPath; } export async function getActorsMCPServerURL(actorID: string, _apifyToken: string): Promise { @@ -11,5 +17,6 @@ export async function getActorsMCPServerURL(actorID: string, _apifyToken: string const standbyBaseUrl = process.env.HOSTNAME === 'mcp-securitybyobscurity.apify.com' ? 'securitybyobscurity.apify.actor' : 'apify.actor'; const standbyUrl = getActorStandbyURL(actorID, standbyBaseUrl); - return `${standbyUrl}/sse`; + const mcpPath = await getActorsMCPServerPath(actorID, _apifyToken); + return `${standbyUrl}/${mcpPath}`; } diff --git a/src/mcp/utils.ts b/src/mcp/utils.ts index f7926c31..25b9f6a3 100644 --- a/src/mcp/utils.ts +++ b/src/mcp/utils.ts @@ -9,6 +9,8 @@ import type { ToolWrap } from '../types.js'; import { addTool, getActorsAsTools, removeTool } from '../tools/index.js'; import { Input } from "../types.js"; import { APIFY_USERNAME } from "../const.js"; +import { ActorDefinition } from "apify-client"; +import { ApifyClient } from "../apify-client.js"; /** * Generates a unique server ID based on the provided URL. @@ -81,3 +83,28 @@ export function getActorStandbyURL(actorID: string, standbyBaseUrl = 'apify.acto return `https://${prefix}${actorName}.${standbyBaseUrl}`; } + +export async function getActorDefinition(actorID: string, apifyToken: string): Promise { + const apifyClient = new ApifyClient({ token: apifyToken + }) + const actor = apifyClient.actor(actorID); + const info = await actor.get(); + if (!info) { + throw new Error(`Actor ${actorID} not found`); + } + const latestBuildID = info.taggedBuilds?.['latest']?.buildId; + if (!latestBuildID) { + throw new Error(`Actor ${actorID} does not have a latest build`); + } + const build = apifyClient.build(latestBuildID); + const buildInfo = await build.get(); + if (!buildInfo) { + throw new Error(`Build ${latestBuildID} not found`); + } + const actorDefinition = buildInfo.actorDefinition; + if (!actorDefinition) { + throw new Error(`Build ${latestBuildID} does not have an actor definition`); + } + + return actorDefinition; +} From 3c92717ddb19592de3c5e4d44c4301d9ae7fe529 Mon Sep 17 00:00:00 2001 From: MQ Date: Tue, 15 Apr 2025 15:18:07 +0200 Subject: [PATCH 15/22] Remove unused import from actor.ts --- src/tools/actor.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tools/actor.ts b/src/tools/actor.ts index c834f149..e5082d05 100644 --- a/src/tools/actor.ts +++ b/src/tools/actor.ts @@ -19,7 +19,6 @@ import { getActorsMCPServerURL, isActorMCPServer } from '../mcp/actors.js'; import { createMCPClient } from '../mcp/client.js'; import { getMCPServerTools } from '../mcp/proxy.js'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import { cp } from 'node:fs'; /** * Calls an Apify actor and retrieves the dataset items. From 1c3dc7a83cbf76fb4570514ed500ee6cdf952500 Mon Sep 17 00:00:00 2001 From: MQ Date: Tue, 15 Apr 2025 15:23:32 +0200 Subject: [PATCH 16/22] console.log to logger --- src/tools/actor.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tools/actor.ts b/src/tools/actor.ts index e5082d05..fa8ad18e 100644 --- a/src/tools/actor.ts +++ b/src/tools/actor.ts @@ -145,8 +145,8 @@ export async function getActorsAsTools( actors: string[], apifyToken: string ): Promise { - console.log('Fetching actors as tools...'); - console.log(actors) + log.debug(`Fetching actors as tools...`); + log.debug(`Actors: ${actors}`); // Actorized MCP servers const actorsMCPServers: string[] = []; for (const actorID of actors) { @@ -156,8 +156,8 @@ export async function getActorsAsTools( } // Normal Actors as a tool const toolActors = actors.filter((actorID) => !actorsMCPServers.includes(actorID)); - console.log('actorsMCPserver', actorsMCPServers); - console.log('toolActors', toolActors); + log.debug(`actorsMCPserver: ${actorsMCPServers}`); + log.debug(`toolActors: ${toolActors}`); // Normal Actors as a tool const normalTools = await getNormalActorsAsTools(toolActors, apifyToken); From 2f6dec62327629d81ce9786b10c9e4c7d63f8bf7 Mon Sep 17 00:00:00 2001 From: MQ Date: Tue, 15 Apr 2025 15:35:18 +0200 Subject: [PATCH 17/22] Remove extra slash from MCP server URL construction --- src/mcp/actors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp/actors.ts b/src/mcp/actors.ts index 7f85c90a..c1bfc7a7 100644 --- a/src/mcp/actors.ts +++ b/src/mcp/actors.ts @@ -18,5 +18,5 @@ export async function getActorsMCPServerURL(actorID: string, _apifyToken: string 'securitybyobscurity.apify.actor' : 'apify.actor'; const standbyUrl = getActorStandbyURL(actorID, standbyBaseUrl); const mcpPath = await getActorsMCPServerPath(actorID, _apifyToken); - return `${standbyUrl}/${mcpPath}`; + return `${standbyUrl}${mcpPath}`; } From 46ef1869cc01af957c65f9b605448b92c245df9b Mon Sep 17 00:00:00 2001 From: MQ Date: Tue, 15 Apr 2025 15:49:18 +0200 Subject: [PATCH 18/22] Simplify actor tools loading logic --- src/main.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main.ts b/src/main.ts index 95b32595..04fd89f8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -40,14 +40,10 @@ if (STANDBY_MODE) { if (input.enableActorAutoLoading) { tools.push(addTool, removeTool); } - if (input.actors === undefined || input.actors === null) { - const actorTools = await getActorsAsTools(defaults.actors, process.env.APIFY_TOKEN as string); - tools.push(...actorTools); - } else { - const actorsToLoad = Array.isArray(input.actors) ? input.actors : input.actors.split(','); - const actorTools = await getActorsAsTools(actorsToLoad, process.env.APIFY_TOKEN as string); - tools.push(...actorTools); - } + const actors = input.actors ?? defaults.actors; + const actorsToLoad = Array.isArray(actors) ? actors : actors.split(','); + const actorTools = await getActorsAsTools(actorsToLoad, process.env.APIFY_TOKEN as string); + tools.push(...actorTools); mcpServer.updateTools(tools); app.listen(PORT, () => { log.info(`The Actor web server is listening for user requests at ${HOST}`); From c39b5efc834a93dc7732e189b82558575e4a77b4 Mon Sep 17 00:00:00 2001 From: MQ Date: Tue, 15 Apr 2025 16:17:38 +0200 Subject: [PATCH 19/22] Refactor actorOwnerDNSFriendly to handle special characters --- src/mcp/utils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/mcp/utils.ts b/src/mcp/utils.ts index 25b9f6a3..4e91e468 100644 --- a/src/mcp/utils.ts +++ b/src/mcp/utils.ts @@ -78,7 +78,11 @@ export function getActorStandbyURL(actorID: string, standbyBaseUrl = 'apify.acto throw new Error(`Invalid actor ID: ${actorID}`); } - const actorOwnerDNSFriendly = actorOwner.replace('.', '-'); + // TODO: get from API + const actorOwnerDNSFriendly = actorOwner + .toLowerCase() + .replace(/[^a-z0-9-]/g, '-') // only alphanumeric chars and hyphens allowed + .replace(/-+/g, '-'); // replace multiple hyphens with one const prefix = actorOwner === APIFY_USERNAME ? '' : `${actorOwnerDNSFriendly}--`; return `https://${prefix}${actorName}.${standbyBaseUrl}`; From 7703eba95b16a04a15a2282c1bf4fdc7c4a8dd9b Mon Sep 17 00:00:00 2001 From: MQ Date: Tue, 15 Apr 2025 23:05:21 +0200 Subject: [PATCH 20/22] Add TODO comment for reworking actor definition fetch logic --- src/tools/actor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tools/actor.ts b/src/tools/actor.ts index fa8ad18e..d57c7e83 100644 --- a/src/tools/actor.ts +++ b/src/tools/actor.ts @@ -150,6 +150,7 @@ export async function getActorsAsTools( // Actorized MCP servers const actorsMCPServers: string[] = []; for (const actorID of actors) { + // TODO: rework, we are fetching actor definition from API twice - in the getMCPServerTools if (await isActorMCPServer(actorID, apifyToken)) { actorsMCPServers.push(actorID); } From 29f35613dc8b4e3b35ec58ae78b3363f8770151d Mon Sep 17 00:00:00 2001 From: MQ Date: Wed, 16 Apr 2025 09:30:37 +0200 Subject: [PATCH 21/22] refactor mcp actor and utils, use real Actor ID as standby URL --- src/mcp/actors.ts | 62 ++++++++++++++++++++++++++++++++++++++++++++--- src/mcp/utils.ts | 52 --------------------------------------- 2 files changed, 58 insertions(+), 56 deletions(-) diff --git a/src/mcp/actors.ts b/src/mcp/actors.ts index c1bfc7a7..8b71a96f 100644 --- a/src/mcp/actors.ts +++ b/src/mcp/actors.ts @@ -1,6 +1,7 @@ import { ActorDefinition } from "apify-client"; -import { getActorDefinition, getActorStandbyURL } from "./utils.js"; +import { ApifyClient } from "../apify-client.js"; + export async function isActorMCPServer(actorID: string, apifyToken: string): Promise { const mcpPath = await getActorsMCPServerPath(actorID, apifyToken); @@ -12,11 +13,64 @@ export async function getActorsMCPServerPath(actorID: string, apifyToken: string return (actorDefinition as any).webServerMcpPath; } -export async function getActorsMCPServerURL(actorID: string, _apifyToken: string): Promise { +export async function getActorsMCPServerURL(actorID: string, apifyToken: string): Promise { // TODO: get from API instead const standbyBaseUrl = process.env.HOSTNAME === 'mcp-securitybyobscurity.apify.com' ? 'securitybyobscurity.apify.actor' : 'apify.actor'; - const standbyUrl = getActorStandbyURL(actorID, standbyBaseUrl); - const mcpPath = await getActorsMCPServerPath(actorID, _apifyToken); + const standbyUrl = await getActorStandbyURL(actorID, apifyToken, standbyBaseUrl); + const mcpPath = await getActorsMCPServerPath(actorID, apifyToken); return `${standbyUrl}${mcpPath}`; } + +/** +* Gets Actor ID from the Actor object. +* +* @param actorID +*/ +export async function getRealActorID(actorID: string, apifyToken: string): Promise { + const apifyClient = new ApifyClient({ token: apifyToken }); + + const actor = apifyClient.actor(actorID); + const info = await actor.get(); + if (!info) { + throw new Error(`Actor ${actorID} not found`); + } + return info.id; +} + +/** +* Returns standby URL for given Actor ID. +* +* @param actorID +* @param standbyBaseUrl +* @returns +*/ +export async function getActorStandbyURL(actorID: string, apifyToken: string, standbyBaseUrl = 'apify.actor'): Promise { + const actorRealID = await getRealActorID(actorID, apifyToken); + return `https://${actorRealID}.${standbyBaseUrl}`; +} + +export async function getActorDefinition(actorID: string, apifyToken: string): Promise { + const apifyClient = new ApifyClient({ token: apifyToken + }) + const actor = apifyClient.actor(actorID); + const info = await actor.get(); + if (!info) { + throw new Error(`Actor ${actorID} not found`); + } + const latestBuildID = info.taggedBuilds?.['latest']?.buildId; + if (!latestBuildID) { + throw new Error(`Actor ${actorID} does not have a latest build`); + } + const build = apifyClient.build(latestBuildID); + const buildInfo = await build.get(); + if (!buildInfo) { + throw new Error(`Build ${latestBuildID} not found`); + } + const actorDefinition = buildInfo.actorDefinition; + if (!actorDefinition) { + throw new Error(`Build ${latestBuildID} does not have an actor definition`); + } + + return actorDefinition; +} diff --git a/src/mcp/utils.ts b/src/mcp/utils.ts index 4e91e468..45cb41df 100644 --- a/src/mcp/utils.ts +++ b/src/mcp/utils.ts @@ -8,9 +8,6 @@ import type { ToolWrap } from '../types.js'; import { addTool, getActorsAsTools, removeTool } from '../tools/index.js'; import { Input } from "../types.js"; -import { APIFY_USERNAME } from "../const.js"; -import { ActorDefinition } from "apify-client"; -import { ApifyClient } from "../apify-client.js"; /** * Generates a unique server ID based on the provided URL. @@ -63,52 +60,3 @@ export function parseInputParamsFromUrl(url: string): Input { const params = parse(query) as unknown as Input; return processInput(params); } - -/** -* Returns standby URL for given Actor ID. -* -* @param actorID -* @param standbyBaseUrl -* @returns -*/ -export function getActorStandbyURL(actorID: string, standbyBaseUrl = 'apify.actor'): string { - const actorOwner = actorID.split('/')[0]; - const actorName = actorID.split('/')[1]; - if (!actorOwner || !actorName) { - throw new Error(`Invalid actor ID: ${actorID}`); - } - - // TODO: get from API - const actorOwnerDNSFriendly = actorOwner - .toLowerCase() - .replace(/[^a-z0-9-]/g, '-') // only alphanumeric chars and hyphens allowed - .replace(/-+/g, '-'); // replace multiple hyphens with one - const prefix = actorOwner === APIFY_USERNAME ? '' : `${actorOwnerDNSFriendly}--`; - - return `https://${prefix}${actorName}.${standbyBaseUrl}`; -} - -export async function getActorDefinition(actorID: string, apifyToken: string): Promise { - const apifyClient = new ApifyClient({ token: apifyToken - }) - const actor = apifyClient.actor(actorID); - const info = await actor.get(); - if (!info) { - throw new Error(`Actor ${actorID} not found`); - } - const latestBuildID = info.taggedBuilds?.['latest']?.buildId; - if (!latestBuildID) { - throw new Error(`Actor ${actorID} does not have a latest build`); - } - const build = apifyClient.build(latestBuildID); - const buildInfo = await build.get(); - if (!buildInfo) { - throw new Error(`Build ${latestBuildID} not found`); - } - const actorDefinition = buildInfo.actorDefinition; - if (!actorDefinition) { - throw new Error(`Build ${latestBuildID} does not have an actor definition`); - } - - return actorDefinition; -} From 3292609f28ee1f42aa01c6e4252fc4fc7ca19666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kopeck=C3=BD?= Date: Wed, 16 Apr 2025 14:43:33 +0200 Subject: [PATCH 22/22] fix: get-actor-definition-default-build (#73) * fix get default build in get actor definition * fix tests * fix double import, lint --- src/apify-client.ts | 6 +++++- src/mcp/actors.ts | 23 +++++++++++++++-------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/apify-client.ts b/src/apify-client.ts index d4bbb708..f3d63e2c 100644 --- a/src/apify-client.ts +++ b/src/apify-client.ts @@ -16,11 +16,15 @@ function addUserAgent(config: AxiosRequestConfig): AxiosRequestConfig { return updatedConfig; } +export function getApifyAPIBaseUrl(): string { + return process.env.APIFY_API_BASE_URL || 'https://api.apify.com'; +} + export class ApifyClient extends _ApifyClient { constructor(options: ApifyClientOptions) { super({ ...options, - baseUrl: process.env.APIFY_API_BASE_URL || undefined, + baseUrl: getApifyAPIBaseUrl(), requestInterceptors: [addUserAgent], }); } diff --git a/src/mcp/actors.ts b/src/mcp/actors.ts index 1c4d4be7..b5a45044 100644 --- a/src/mcp/actors.ts +++ b/src/mcp/actors.ts @@ -1,6 +1,6 @@ import type { ActorDefinition } from 'apify-client'; -import { ApifyClient } from '../apify-client.js'; +import { ApifyClient, getApifyAPIBaseUrl } from '../apify-client.js'; export async function isActorMCPServer(actorID: string, apifyToken: string): Promise { const mcpPath = await getActorsMCPServerPath(actorID, apifyToken); @@ -63,18 +63,25 @@ export async function getActorDefinition(actorID: string, apifyToken: string): P if (!info) { throw new Error(`Actor ${actorID} not found`); } - const latestBuildID = info.taggedBuilds?.latest?.buildId; - if (!latestBuildID) { - throw new Error(`Actor ${actorID} does not have a latest build`); + + const actorObjID = info.id; + const res = await fetch(`${getApifyAPIBaseUrl()}/v2/acts/${actorObjID}/builds/default`, { + headers: { + // This is done so tests can pass with public Actors without token + ...(apifyToken ? { Authorization: `Bearer ${apifyToken}` } : {}), + }, + }); + if (!res.ok) { + throw new Error(`Failed to fetch default build for actor ${actorID}: ${res.statusText}`); } - const build = apifyClient.build(latestBuildID); - const buildInfo = await build.get(); + const json = await res.json() as any; // eslint-disable-line @typescript-eslint/no-explicit-any + const buildInfo = json.data; if (!buildInfo) { - throw new Error(`Build ${latestBuildID} not found`); + throw new Error(`Default build for Actor ${actorID} not found`); } const { actorDefinition } = buildInfo; if (!actorDefinition) { - throw new Error(`Build ${latestBuildID} does not have an actor definition`); + throw new Error(`Actor default build ${actorID} does not have Actor definition`); } return actorDefinition;