From 332a59e3d470fbbde01e638b9022ad1226d865e0 Mon Sep 17 00:00:00 2001 From: Jiri Spilka Date: Tue, 23 Sep 2025 23:01:29 +0200 Subject: [PATCH 1/5] fix: WIP markdown fix --- src/tools/actor.ts | 2 +- src/tools/fetch-actor-details.ts | 6 +++--- src/tools/store_collection.ts | 9 ++++++--- src/utils/actor-card.ts | 14 +++++++------- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/tools/actor.ts b/src/tools/actor.ts index 6eb6c67e..1e50e29d 100644 --- a/src/tools/actor.ts +++ b/src/tools/actor.ts @@ -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. diff --git a/src/tools/fetch-actor-details.ts b/src/tools/fetch-actor-details.ts index 1fb22f39..f75ef912 100644 --- a/src/tools/fetch-actor-details.ts +++ b/src/tools/fetch-actor-details.ts @@ -42,9 +42,9 @@ USAGE EXAMPLES: } 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)}` }, + { type: 'text', text: `# Actor information\n${details.actorCard}` }, + { type: 'text', text: `# README\n${details.readme}` }, + { type: 'text', text: `# Input schema\n${JSON.stringify(details.inputSchema, null, 0)}` }, ], }; }, diff --git a/src/tools/store_collection.ts b/src/tools/store_collection.ts index 1ad5d6ac..7a844645 100644 --- a/src/tools/store_collection.ts +++ b/src/tools/store_collection.ts @@ -104,9 +104,12 @@ USAGE EXAMPLES: 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 query:** ${parsed.search} +**Number of Actors found:** ${actorCards.length} +**Actor cards:** + +${actorCards.join('\n\n')}`, }, ], }; diff --git a/src/utils/actor-card.ts b/src/utils/actor-card.ts index 2a699979..d2a07c79 100644 --- a/src/utils/actor-card.ts +++ b/src/utils/actor-card.ts @@ -45,11 +45,12 @@ export function formatActorToActorCard( // 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} (\`${actorFullName}\`)`, + `- **URL:** ${APIFY_STORE_URL}/${actorFullName}`, + `- **Developed by:** [${actor.username}](${APIFY_STORE_URL}) ${actor.username === 'apify' ? '(Apify)' : '(community)'}`, + `- **Description:** ${actor.description || 'No description provided.'}`, + `- **Categories:** ${formattedCategories.length ? formattedCategories.join(', ') : 'Uncategorized'}`, + `- **Pricing:** ${pricingInfo}`, ]; // Add stats - handle different stat structures @@ -111,7 +112,6 @@ export function formatActorsListToActorCard(actors: (Actor | ExtendedActorStoreL return []; } return actors.map((actor) => { - const card = formatActorToActorCard(actor); - return `- ${card}`; + return formatActorToActorCard(actor); }); } From 4ccb4f6ba97664d7582e15ce416673341d29a991 Mon Sep 17 00:00:00 2001 From: Jiri Spilka Date: Wed, 24 Sep 2025 11:52:57 +0200 Subject: [PATCH 2/5] fix: update Actor card --- src/tools/fetch-actor-details.ts | 24 +++++++++++++++++------- src/tools/store_collection.ts | 8 +++++--- src/utils/actor-card.ts | 15 ++++++++------- src/utils/pricing-info.ts | 21 +++++++++++++++++---- 4 files changed, 47 insertions(+), 21 deletions(-) diff --git a/src/tools/fetch-actor-details.ts b/src/tools/fetch-actor-details.ts index f75ef912..90d7178f 100644 --- a/src/tools/fetch-actor-details.ts +++ b/src/tools/fetch-actor-details.ts @@ -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 information\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, }; diff --git a/src/tools/store_collection.ts b/src/tools/store_collection.ts index 7a844645..fd5bbc53 100644 --- a/src/tools/store_collection.ts +++ b/src/tools/store_collection.ts @@ -105,9 +105,11 @@ USAGE EXAMPLES: { type: 'text', text: ` -**Search query:** ${parsed.search} -**Number of Actors found:** ${actorCards.length} -**Actor cards:** +# Search results: +- **Search query:** ${parsed.search} +- **Number of Actors found:** ${actorCards.length} + +# Actors: ${actorCards.join('\n\n')}`, }, diff --git a/src/utils/actor-card.ts b/src/utils/actor-card.ts index d2a07c79..409bd4fb 100644 --- a/src/utils/actor-card.ts +++ b/src/utils/actor-card.ts @@ -42,15 +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} (\`${actorFullName}\`)`, - `- **URL:** ${APIFY_STORE_URL}/${actorFullName}`, - `- **Developed by:** [${actor.username}](${APIFY_STORE_URL}) ${actor.username === 'apify' ? '(Apify)' : '(community)'}`, + `## [${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:** ${pricingInfo}`, + `- **[Pricing](${actorUrl}/pricing):** ${pricingInfo}`, ]; // Add stats - handle different stat structures @@ -81,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 diff --git a/src/utils/pricing-info.ts b/src/utils/pricing-info.ts index cc9a00e9..c6af9c0b 100644 --- a/src/utils/pricing-info.ts +++ b/src/utils/pricing-info.ts @@ -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.'; 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) { @@ -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 'User pays for the computing resources consumed by the Actor'; } if (pricingInfo.pricingModel === ACTOR_PRICING_MODEL.PRICE_PER_DATASET_ITEM) { const customUnitName = pricingInfo.unitName !== 'result' ? pricingInfo.unitName : ''; @@ -92,5 +105,5 @@ export function pricingInfoToString(pricingInfo: ExtendedPricingInfo | null): st if (pricingInfo.pricingModel === ACTOR_PRICING_MODEL.PAY_PER_EVENT) { return payPerEventPricingToString(pricingInfo.pricingPerEvent); } - return 'unknown'; + return 'Not available'; } From ea0f845011341b8943857a55842e94bf516ba859 Mon Sep 17 00:00:00 2001 From: Jiri Spilka Date: Wed, 24 Sep 2025 14:18:45 +0200 Subject: [PATCH 3/5] fix: handle review comments --- src/tools/actor.ts | 6 +++--- src/tools/store_collection.ts | 11 ++++++++--- src/utils/actor-card.ts | 14 -------------- src/utils/pricing-info.ts | 10 +++++----- 4 files changed, 16 insertions(+), 25 deletions(-) diff --git a/src/tools/actor.ts b/src/tools/actor.ts index 1e50e29d..5c0fb589 100644 --- a/src/tools/actor.ts +++ b/src/tools/actor.ts @@ -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}"`]); @@ -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. @@ -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)}`, ]); } } diff --git a/src/tools/store_collection.ts b/src/tools/store_collection.ts index fd5bbc53..4c7f9b1b 100644 --- a/src/tools/store_collection.ts +++ b/src/tools/store_collection.ts @@ -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( @@ -99,7 +99,12 @@ 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: [ { @@ -111,7 +116,7 @@ USAGE EXAMPLES: # Actors: -${actorCards.join('\n\n')}`, +${actorsText}`, }, ], }; diff --git a/src/utils/actor-card.ts b/src/utils/actor-card.ts index 409bd4fb..5e84884e 100644 --- a/src/utils/actor-card.ts +++ b/src/utils/actor-card.ts @@ -102,17 +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) => { - return formatActorToActorCard(actor); - }); -} diff --git a/src/utils/pricing-info.ts b/src/utils/pricing-info.ts index c6af9c0b..3ff33ef9 100644 --- a/src/utils/pricing-info.ts +++ b/src/utils/pricing-info.ts @@ -55,7 +55,7 @@ function convertMinutesToGreatestUnit(minutes: number): { value: number; unit: s */ 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 = `\t- **${event.eventTitle}**: ${event.eventDescription} `; @@ -78,7 +78,7 @@ export function pricingInfoToString(pricingInfo: ExtendedPricingInfo | null): st // 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 'User 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 : ''; @@ -98,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 'Not available'; + return 'Pricing information is not available.'; } From bb905d2cfb61c6a1cf061936cf135ed1c4e30943 Mon Sep 17 00:00:00 2001 From: Jiri Spilka Date: Wed, 24 Sep 2025 14:21:14 +0200 Subject: [PATCH 4/5] fix: handle review comments --- src/utils/actor-card.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/actor-card.ts b/src/utils/actor-card.ts index 5e84884e..d82f3937 100644 --- a/src/utils/actor-card.ts +++ b/src/utils/actor-card.ts @@ -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 */ From 7c4d02a6a5f25f1ebeba940cc36d5c60b1a7a267 Mon Sep 17 00:00:00 2001 From: Jiri Spilka Date: Wed, 24 Sep 2025 15:13:09 +0200 Subject: [PATCH 5/5] fix: trigger pr-toolkit --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index b32318d5..572f818b 100644 --- a/README.md +++ b/README.md @@ -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 ` 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.