Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
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