Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ For example, it can:
You can use the Apify MCP Server in two ways:

**HTTPS Endpoint (mcp.apify.com)**: Connect from your MCP client via OAuth or by including the `Authorization: Bearer <APIFY_TOKEN>` header in your requests. This is the recommended method for most use cases. Because it supports OAuth, you can connect from clients like [Claude.ai](https://claude.ai) or [Visual Studio Code](https://code.visualstudio.com/) using just the URL: `https://mcp.apify.com`.
- `https://mcp.apify.com` (recommended) for streamable transport
- `https://mcp.apify.com/sse` for legacy SSE transport
- `https://mcp.apify.com` streamable transport

**Standard Input/Output (stdio)**: Ideal for local integrations and command-line tools like the Claude for Desktop client.
- Set the MCP client server command to `npx @apify/actors-mcp-server` and the `APIFY_TOKEN` environment variable to your Apify API token.
Expand Down
6 changes: 3 additions & 3 deletions src/tools/actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ EXAMPLES:
}
const toolsResponse = await client.listTools();

const toolsInfo = toolsResponse.tools.map((tool) => `**${tool.name}**\n${tool.description || 'No description'}\nInput Schema: ${JSON.stringify(tool.inputSchema, null, 2)}`,
const toolsInfo = toolsResponse.tools.map((tool) => `**${tool.name}**\n${tool.description || 'No description'}\nInput schema: ${JSON.stringify(tool.inputSchema, null, 2)}`,
).join('\n\n');

return buildMCPResponse([`This is an MCP Server Actor with the following tools:\n\n${toolsInfo}\n\nTo call a tool, use step="call" with actor name format: "${baseActorName}:{toolName}"`]);
Expand All @@ -420,7 +420,7 @@ EXAMPLES:
}
const content = [
// TODO: update result to say: this is result of info step, you must now call again with step=call and proper input
{ type: 'text', text: `**Input Schema:**\n${JSON.stringify(details.inputSchema, null, 0)}` },
{ type: 'text', text: `Input schema: \n${JSON.stringify(details.inputSchema, null, 0)}` },
];
/**
* Add Skyfire instructions also in the info step since clients are most likely truncating the long tool description of the call-actor.
Expand Down Expand Up @@ -499,7 +499,7 @@ EXAMPLES:
if (errors && errors.length > 0) {
return buildMCPResponse([
`Input validation failed for Actor '${actorName}': ${errors.map((e) => e.message).join(', ')}`,
`Input Schema:\n${JSON.stringify(actor.tool.inputSchema)}`,
`Input schema:\n${JSON.stringify(actor.tool.inputSchema)}`,
]);
}
}
Expand Down
24 changes: 17 additions & 7 deletions src/tools/fetch-actor-details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,23 @@ USAGE EXAMPLES:
content: [{ type: 'text', text: `Actor information for '${parsed.actor}' was not found. Please check the Actor ID or name and ensure the Actor exists.` }],
};
}
return {
content: [
{ type: 'text', text: `**Actor card**:\n${details.actorCard}` },
{ type: 'text', text: `**README:**\n${details.readme}` },
{ type: 'text', text: `**Input Schema:**\n${JSON.stringify(details.inputSchema, null, 0)}` },
],
};

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}` },
];

// 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, null, 0)}\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 };
},
} as InternalTool,
};
20 changes: 15 additions & 5 deletions src/tools/store_collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import zodToJsonSchema from 'zod-to-json-schema';
import { ApifyClient } from '../apify-client.js';
import { ACTOR_SEARCH_ABOVE_LIMIT, HelperTools } from '../const.js';
import type { ActorPricingModel, ExtendedActorStoreList, HelperTool, ToolEntry } from '../types.js';
import { formatActorsListToActorCard } from '../utils/actor-card.js';
import { formatActorToActorCard } from '../utils/actor-card.js';
import { ajv } from '../utils/ajv.js';

export async function searchActorsByKeywords(
Expand Down Expand Up @@ -99,14 +99,24 @@ USAGE EXAMPLES:
parsed.offset,
);
actors = filterRentalActors(actors || [], userRentedActorIds || []).slice(0, parsed.limit);
const actorCards = formatActorsListToActorCard(actors);
const actorCards = actors.length === 0 ? [] : actors.map(formatActorToActorCard);

const actorsText = actorCards.length
? actorCards.join('\n\n')
: 'No Actors were found for the given search query. Please try different keywords or simplify your query.';

return {
content: [
{
type: 'text',
text: `**Search query:** ${parsed.search}\n\n`
+ `**Number of Actors found:** ${actorCards.length}\n\n`
+ `**Actor cards:**\n${actorCards.join('\n\n')}`,
text: `
# Search results:
- **Search query:** ${parsed.search}
- **Number of Actors found:** ${actorCards.length}

# Actors:

${actorsText}`,
},
],
};
Expand Down
35 changes: 11 additions & 24 deletions src/utils/actor-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function formatCategories(categories?: string[]): string[] {
}

/**
* Formats Actor details into an Actor card (Actor markdown representation).
* Formats Actor details into an Actor card (Actor information in markdown).
* @param actor - Actor information from the API
* @returns Formatted actor card
*/
Expand All @@ -42,14 +42,16 @@ export function formatActorToActorCard(
}

const actorFullName = `${actor.username}/${actor.name}`;
const actorUrl = `${APIFY_STORE_URL}/${actorFullName}`;

// Build the markdown lines
const markdownLines = [
`# [${actor.title}](${APIFY_STORE_URL}/${actorFullName}) (${actorFullName})`,
`**Developed by:** ${actor.username} ${actor.username === 'apify' ? '(Apify)' : '(community)'}`,
`**Description:** ${actor.description || 'No description provided.'}`,
`**Categories:** ${formattedCategories.length ? formattedCategories.join(', ') : 'Uncategorized'}`,
`**Pricing:** ${pricingInfo}`,
`## [${actor.title}](${actorUrl}) (\`${actorFullName}\`)`,
`- **URL:** ${actorUrl}`,
`- **Developed by:** [${actor.username}](${APIFY_STORE_URL}/${actor.username}) ${actor.username === 'apify' ? '(Apify)' : '(community)'}`,
`- **Description:** ${actor.description || 'No description provided.'}`,
`- **Categories:** ${formattedCategories.length ? formattedCategories.join(', ') : 'Uncategorized'}`,
`- **[Pricing](${actorUrl}/pricing):** ${pricingInfo}`,
];

// Add stats - handle different stat structures
Expand Down Expand Up @@ -80,18 +82,18 @@ export function formatActorToActorCard(
}

if (statsParts.length > 0) {
markdownLines.push(`**Stats:** ${statsParts.join(', ')}`);
markdownLines.push(`- **Stats:** ${statsParts.join(', ')}`);
}
}

// Add rating if available (ActorStoreList only)
if ('actorReviewRating' in actor && actor.actorReviewRating) {
markdownLines.push(`**Rating:** ${actor.actorReviewRating.toFixed(2)} out of 5`);
markdownLines.push(`- **Rating:** ${actor.actorReviewRating.toFixed(2)} out of 5`);
}

// Add modification date if available
if ('modifiedAt' in actor) {
markdownLines.push(`**Last modified:** ${actor.modifiedAt.toISOString()}`);
markdownLines.push(`- **Last modified:** ${actor.modifiedAt.toISOString()}`);
}

// Add deprecation warning if applicable
Expand All @@ -100,18 +102,3 @@ export function formatActorToActorCard(
}
return markdownLines.join('\n');
}

/**
* Formats a list of Actors into Actor cards
* @param actors - Array of Actor information
* @returns Formatted markdown string
*/
export function formatActorsListToActorCard(actors: (Actor | ExtendedActorStoreList)[]): string[] {
if (actors.length === 0) {
return [];
}
return actors.map((actor) => {
const card = formatActorToActorCard(actor);
return `- ${card}`;
});
}
27 changes: 20 additions & 7 deletions src/utils/pricing-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,24 @@ function convertMinutesToGreatestUnit(minutes: number): { value: number; unit: s
return { value: Math.floor(minutes / (60 * 24)), unit: 'days' };
}

/**
* Formats the pay-per-event pricing information into a human-readable string.
*
* Example:
* This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for the following events:
* - Event title: Event description (Flat price: $X per event)
* - MCP server startup: Initial fee for starting the Kiwi MCP Server Actor (Flat price: $0.1 per event)
* - Flight search: Fee for searching flights using the Kiwi.com flight search engine (Flat price: $0.001 per event)
*
* For tiered pricing, the output is more complicated and the question is whether we want to simplify it in the future.
* @param pricingPerEvent
*/

function payPerEventPricingToString(pricingPerEvent: ExtendedPricingInfo['pricingPerEvent']): string {
if (!pricingPerEvent || !pricingPerEvent.actorChargeEvents) return 'No event pricing information available.';
if (!pricingPerEvent || !pricingPerEvent.actorChargeEvents) return 'Pricing information for events is not available.';
const eventStrings: string[] = [];
for (const event of Object.values(pricingPerEvent.actorChargeEvents)) {
let eventStr = `- ${event.eventTitle}: ${event.eventDescription} `;
let eventStr = `\t- **${event.eventTitle}**: ${event.eventDescription} `;
if (typeof event.eventPriceUsd === 'number') {
eventStr += `(Flat price: $${event.eventPriceUsd} per event)`;
} else if (event.eventTieredPricingUsd) {
Expand All @@ -58,14 +71,14 @@ function payPerEventPricingToString(pricingPerEvent: ExtendedPricingInfo['pricin
}
eventStrings.push(eventStr);
}
return `This Actor charges per event as follows:\n${eventStrings.join('\n')}`;
return `This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for the following events:\n${eventStrings.join('\n')}`;
}

export function pricingInfoToString(pricingInfo: ExtendedPricingInfo | null): string {
// If there is no pricing infos entries the Actor is free to use
// based on https://github.com/apify/apify-core/blob/058044945f242387dde2422b8f1bef395110a1bf/src/packages/actor/src/paid_actors/paid_actors_common.ts#L691
if (pricingInfo === null || pricingInfo.pricingModel === ACTOR_PRICING_MODEL.FREE) {
return 'This Actor is free to use; the user only pays for the computing resources consumed by the Actor.';
return 'This Actor is free to use. You are only charged for Apify platform usage.';
}
if (pricingInfo.pricingModel === ACTOR_PRICING_MODEL.PRICE_PER_DATASET_ITEM) {
const customUnitName = pricingInfo.unitName !== 'result' ? pricingInfo.unitName : '';
Expand All @@ -85,12 +98,12 @@ export function pricingInfoToString(pricingInfo: ExtendedPricingInfo | null): st
const tiers = Object.entries(pricingInfo.tieredPricing)
.map(([tier, obj]) => `${tier}: $${obj.tieredPricePerUnitUsd} per month`)
.join(', ');
return `This Actor is rental and thus has tiered pricing per month: ${tiers}, with a trial period of ${value} ${unit}.`;
return `This Actor is rental and has tiered pricing per month: ${tiers}, with a trial period of ${value} ${unit}.`;
}
return `This Actor is rental and thus has a flat price of ${pricingInfo.pricePerUnitUsd} USD per month, with a trial period of ${value} ${unit}.`;
return `This Actor is rental and has a flat price of ${pricingInfo.pricePerUnitUsd} USD per month, with a trial period of ${value} ${unit}.`;
}
if (pricingInfo.pricingModel === ACTOR_PRICING_MODEL.PAY_PER_EVENT) {
return payPerEventPricingToString(pricingInfo.pricingPerEvent);
}
return 'unknown';
return 'Pricing information is not available.';
}
Loading