Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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 evals/evaluation-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export function filterById(testCases: TestCase[], idPattern: string): TestCase[]
export async function loadTools(): Promise<ToolBase[]> {
const apifyClient = new ApifyClient({ token: process.env.APIFY_API_TOKEN || '' });
const urlTools = await processParamsGetTools('', apifyClient);
return urlTools.map((t: ToolEntry) => getToolPublicFieldOnly(t.tool)) as ToolBase[];
return urlTools.map((t: ToolEntry) => getToolPublicFieldOnly(t)) as ToolBase[];
}

export function transformToolsToOpenAIFormat(tools: ToolBase[]): OpenAI.Chat.Completions.ChatCompletionTool[] {
Expand Down
48 changes: 26 additions & 22 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ export class ActorsMcpServer {
* Loads missing toolNames from a provided list of tool names.
* Skips toolNames that are already loaded and loads only the missing ones.
* @param toolNames - Array of tool names to ensure are loaded
* @param apifyToken - Apify API token for authentication
* @param apifyClient
*/
public async loadToolsByName(toolNames: string[], apifyClient: ApifyClient) {
const loadedTools = this.listAllToolNames();
Expand Down Expand Up @@ -215,7 +215,7 @@ export class ActorsMcpServer {
* Load actors as tools, upsert them to the server, and return the tool entries.
* This is a public method that wraps getActorsAsTools and handles the upsert operation.
* @param actorIdsOrNames - Array of actor IDs or names to load as tools
* @param apifyToken - Apify API token for authentication
* @param apifyClient
* @returns Promise<ToolEntry[]> - Array of loaded tool entries
*/
public async loadActorsAsTools(actorIdsOrNames: string[], apifyClient: ApifyClient): Promise<ToolEntry[]> {
Expand Down Expand Up @@ -483,7 +483,9 @@ export class ActorsMcpServer {

// Validate token
if (!apifyToken && !this.options.skyfireMode) {
const msg = 'APIFY_TOKEN is required. It must be set in the environment variables or passed as a parameter in the body.';
const msg = `APIFY_TOKEN is required but was not provided.
Please set the APIFY_TOKEN environment variable or pass it as a parameter in the request body.
You can obtain your Apify token from https://console.apify.com/account/integrations.`;
log.softFail(msg, { statusCode: 400 });
await this.server.sendLoggingMessage({ level: 'error', data: msg });
throw new McpError(
Expand All @@ -507,7 +509,10 @@ export class ActorsMcpServer {
const tool = Array.from(this.tools.values())
.find((t) => t.name === name || (t.type === 'actor' && t.actorFullName === name));
if (!tool) {
const msg = `Tool ${name} not found. Available tools: ${this.listToolNames().join(', ')}`;
const availableTools = this.listToolNames();
const msg = `Tool "${name}" was not found.
Available tools: ${availableTools.length > 0 ? availableTools.join(', ') : 'none'}.
Please verify the tool name is correct. You can list all available tools using the tools/list request.`;
log.softFail(msg, { statusCode: 404 });
await this.server.sendLoggingMessage({ level: 'error', data: msg });
throw new McpError(
Expand All @@ -516,7 +521,8 @@ export class ActorsMcpServer {
);
}
if (!args) {
const msg = `Missing arguments for tool ${name}`;
const msg = `Missing arguments for tool "${name}".
Please provide the required arguments for this tool. Check the tool's input schema using ${HelperTools.ACTOR_GET_DETAILS} tool to see what parameters are required.`;
log.softFail(msg, { statusCode: 400 });
await this.server.sendLoggingMessage({ level: 'error', data: msg });
throw new McpError(
Expand All @@ -529,7 +535,11 @@ export class ActorsMcpServer {
args = decodeDotPropertyNames(args);
log.debug('Validate arguments for tool', { toolName: tool.name, input: args });
if (!tool.ajvValidate(args)) {
const msg = `Invalid arguments for tool ${tool.name}: args: ${JSON.stringify(args)} error: ${JSON.stringify(tool?.ajvValidate.errors)}`;
const errors = tool?.ajvValidate.errors || [];
const errorMessages = errors.map((e: { message?: string; instancePath?: string }) => `${e.instancePath || 'root'}: ${e.message || 'validation error'}`).join('; ');
const msg = `Invalid arguments for tool "${tool.name}".
Validation errors: ${errorMessages}.
Please check the tool's input schema using ${HelperTools.ACTOR_GET_DETAILS} tool and ensure all required parameters are provided with correct types and values.`;
log.softFail(msg, { statusCode: 400 });
await this.server.sendLoggingMessage({ level: 'error', data: msg });
throw new McpError(
Expand Down Expand Up @@ -569,16 +579,11 @@ export class ActorsMcpServer {
try {
client = await connectMCPClient(tool.serverUrl, apifyToken);
if (!client) {
const msg = `Failed to connect to MCP server ${tool.serverUrl}`;
// Note: Timeout errors are already logged as warning in connectMCPClient
// This is a fallback log for when connection fails (client-side issue)
const msg = `Failed to connect to MCP server at "${tool.serverUrl}".
Please verify the server URL is correct and accessible, and ensure you have a valid Apify token with appropriate permissions.`;
log.softFail(msg, { statusCode: 408 }); // 408 Request Timeout
await this.server.sendLoggingMessage({ level: 'error', data: msg });
return {
content: [
{ type: 'text', text: msg },
],
};
return buildMCPResponse([msg]);
}

// Only set up notification handlers if progressToken is provided by the client
Expand Down Expand Up @@ -619,12 +624,7 @@ export class ActorsMcpServer {
if (this.options.skyfireMode
&& args['skyfire-pay-id'] === undefined
) {
return {
content: [{
type: 'text',
text: SKYFIRE_TOOL_INSTRUCTIONS,
}],
};
return buildMCPResponse([SKYFIRE_TOOL_INSTRUCTIONS]);
}

// Create progress tracker if progressToken is available
Expand Down Expand Up @@ -669,11 +669,15 @@ export class ActorsMcpServer {
logHttpError(error, 'Error occurred while calling tool', { toolName: name });
const errorMessage = (error instanceof Error) ? error.message : 'Unknown error';
return buildMCPResponse([
`Error calling tool ${name}: ${errorMessage}`,
`Error calling tool "${name}": ${errorMessage}.
Please verify the tool name, input parameters, and ensure all required resources are available.`,
]);
}

const msg = `Unknown tool: ${name}`;
const availableTools = this.listToolNames();
const msg = `Unknown tool type for "${name}".
Available tools: ${availableTools.length > 0 ? availableTools.join(', ') : 'none'}.
Please verify the tool name and ensure the tool is properly registered.`;
log.softFail(msg, { statusCode: 404 });
await this.server.sendLoggingMessage({
level: 'error',
Expand Down
5 changes: 3 additions & 2 deletions src/mcp/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ export function getProxyMCPServerToolName(url: string, toolName: string): string
/**
* Process input parameters from URL and get tools
* If URL contains query parameter `actors`, return tools from Actors otherwise return null.
* @param url
* @param apifyToken
* @param url The URL to process
* @param apifyClient The Apify client instance
* @param initializeRequestData Optional initialize request data
*/
export async function processParamsGetTools(url: string, apifyClient: ApifyClient, initializeRequestData?: InitializeRequest) {
const input = parseInputParamsFromUrl(url);
Expand Down
16 changes: 12 additions & 4 deletions src/tools/actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,9 @@ EXAMPLES:

// Standby Actors, thus MCPs, are not supported in Skyfire mode
if (isActorMcpServer && apifyMcpServer.options.skyfireMode) {
return buildMCPResponse([`MCP server Actors are not supported in Skyfire mode. Please use a regular Apify token without Skyfire.`]);
return buildMCPResponse([
`This Actor (${actorName}) is an MCP server and cannot be accessed using a Skyfire token. To use this Actor, please provide a valid Apify token instead of a Skyfire token.`,
]);
}

try {
Expand Down Expand Up @@ -436,7 +438,9 @@ EXAMPLES:
// Regular actor: return schema
const details = await fetchActorDetails(apifyClientForDefinition, baseActorName);
if (!details) {
return buildMCPResponse([`Actor information for '${baseActorName}' was not found. Please check the Actor ID or name and ensure the Actor exists.`]);
return buildMCPResponse([`Actor information for '${baseActorName}' was not found.
Please verify Actor ID or name format (e.g., "username/name" like "apify/rag-web-browser") and ensure that the Actor exists.
You can search for available Actors using the tool: ${HelperTools.STORE_SEARCH}.`]);
}
const content = [
`Actor name: ${actorName}`,
Expand Down Expand Up @@ -513,7 +517,9 @@ EXAMPLES:
const [actor] = await getActorsAsTools([actorName], apifyClient);

if (!actor) {
return buildMCPResponse([`Actor '${actorName}' was not found.`]);
return buildMCPResponse([`Actor '${actorName}' was not found.
Please verify Actor ID or name format (e.g., "username/name" like "apify/rag-web-browser") and ensure that the Actor exists.
You can search for available Actors using the tool: ${HelperTools.STORE_SEARCH}.`]);
}

if (!actor.ajvValidate(input)) {
Expand Down Expand Up @@ -548,7 +554,9 @@ EXAMPLES:
return { content };
} catch (error) {
logHttpError(error, 'Failed to call Actor', { actorName, performStep });
return buildMCPResponse([`Failed to call Actor '${actorName}': ${error instanceof Error ? error.message : String(error)}`]);
return buildMCPResponse([`Failed to call Actor '${actorName}': ${error instanceof Error ? error.message : String(error)}.
Please verify the Actor name, input parameters, and ensure the Actor exists.
You can search for available Actors using the tool: ${HelperTools.STORE_SEARCH}, or get Actor details using: ${HelperTools.ACTOR_GET_DETAILS}.`]);
}
},
};
2 changes: 1 addition & 1 deletion src/tools/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import { filterSchemaProperties, shortenProperties } from './utils.js';
* First, fetch the Actor details to get the default build tag and buildId.
* Then, fetch the build details and return actorName, description, and input schema.
* @param {string} actorIdOrName - Actor ID or Actor full name.
* @param {ApifyClient} apifyClient - The Apify client instance.
* @param {number} limit - Truncate the README to this limit.
* @param {string} apifyToken
* @returns {Promise<ActorDefinitionWithDesc | null>} - The actor definition with description or null if not found.
*/
export async function getActorDefinition(
Expand Down
19 changes: 11 additions & 8 deletions src/tools/fetch-actor-details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { HelperTools } from '../const.js';
import type { InternalToolArgs, ToolEntry, ToolInputSchema } from '../types.js';
import { fetchActorDetails } from '../utils/actor-details.js';
import { ajv } from '../utils/ajv.js';
import { buildMCPResponse } from '../utils/mcp.js';

const fetchActorDetailsToolArgsSchema = z.object({
actor: z.string()
Expand Down Expand Up @@ -40,26 +41,28 @@ USAGE EXAMPLES:
const apifyClient = new ApifyClient({ token: apifyToken });
const details = await fetchActorDetails(apifyClient, parsed.actor);
if (!details) {
return {
content: [{ type: 'text', text: `Actor information for '${parsed.actor}' was not found. Please check the Actor ID or name and ensure the Actor exists.` }],
};
const texts = [`Actor information for '${parsed.actor}' was not found.
Please verify Actor ID or name format and ensure that the Actor exists.
You can search for available Actors using the tool: ${HelperTools.STORE_SEARCH}.`,
];
return buildMCPResponse(texts);
}

const actorUrl = `https://apify.com/${details.actorInfo.username}/${details.actorInfo.name}`;
// Add link to README title
details.readme = details.readme.replace(/^# /, `# [README](${actorUrl}/readme): `);

const content = [
{ type: 'text', text: `# Actor information\n${details.actorCard}` },
{ type: 'text', text: `${details.readme}` },
const texts = [
`# Actor information\n${details.actorCard}`,
`${details.readme}`,
];

// Include input schema if it has properties
if (details.inputSchema.properties || Object.keys(details.inputSchema.properties).length !== 0) {
content.push({ type: 'text', text: `# [Input schema](${actorUrl}/input)\n\`\`\`json\n${JSON.stringify(details.inputSchema)}\n\`\`\`` });
texts.push(`# [Input schema](${actorUrl}/input)\n\`\`\`json\n${JSON.stringify(details.inputSchema)}\n\`\`\``);
}
// Return the actor card, README, and input schema (if it has non-empty properties) as separate text blocks
// This allows better formatting in the final output
return { content };
return buildMCPResponse(texts);
},
} as const;
36 changes: 11 additions & 25 deletions src/tools/fetch-apify-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { InternalToolArgs, ToolEntry, ToolInputSchema } from '../types.js';
import { ajv } from '../utils/ajv.js';
import { htmlToMarkdown } from '../utils/html-to-md.js';
import { logHttpError } from '../utils/logging.js';
import { buildMCPResponse } from '../utils/mcp.js';

const fetchApifyDocsToolArgsSchema = z.object({
url: z.string()
Expand Down Expand Up @@ -42,12 +43,9 @@ USAGE EXAMPLES:

// Only allow URLs starting with https://docs.apify.com
if (!url.startsWith('https://docs.apify.com')) {
return {
content: [{
type: 'text',
text: `Only URLs starting with https://docs.apify.com are allowed.`,
}],
};
return buildMCPResponse([`Invalid URL: "${url}".
Only URLs starting with "https://docs.apify.com" are allowed.
Please provide a valid Apify documentation URL. You can find documentation URLs using the ${HelperTools.DOCS_SEARCH} tool.`]);
}

// Cache URL without fragment to avoid fetching the same page multiple times
Expand All @@ -57,17 +55,13 @@ USAGE EXAMPLES:
try {
const response = await fetch(url);
if (!response.ok) {
// Create error object with statusCode for logHttpError
const error = Object.assign(new Error(`HTTP ${response.status} ${response.statusText}`), {
statusCode: response.status,
});
logHttpError(error, 'Failed to fetch the documentation page', { url, statusText: response.statusText });
return {
content: [{
type: 'text',
text: `Failed to fetch the documentation page at ${url}. Status: ${response.status} ${response.statusText}`,
}],
};
return buildMCPResponse([`Failed to fetch the documentation page at "${url}".
HTTP Status: ${response.status} ${response.statusText}.
Please verify the URL is correct and accessible. You can search for available documentation pages using the ${HelperTools.DOCS_SEARCH} tool.`]);
}
const html = await response.text();
markdown = htmlToMarkdown(html);
Expand All @@ -76,20 +70,12 @@ USAGE EXAMPLES:
fetchApifyDocsCache.set(urlWithoutFragment, markdown);
} catch (error) {
logHttpError(error, 'Failed to fetch the documentation page', { url });
return {
content: [{
type: 'text',
text: `Failed to fetch the documentation page at ${url}. Please check the URL and try again.`,
}],
};
return buildMCPResponse([`Failed to fetch the documentation page at "${url}".
Error: ${error instanceof Error ? error.message : String(error)}.
Please verify the URL is correct and accessible. You can search for available documentation pages using the ${HelperTools.DOCS_SEARCH} tool.`]);
}
}

return {
content: [{
type: 'text',
text: `Fetched content from ${url}:\n\n${markdown}`,
}],
};
return buildMCPResponse([`Fetched content from ${url}:\n\n${markdown}`]);
},
} as const;
Loading