Skip to content

Commit 73e3115

Browse files
authored
fix: Actor card markdown (#285)
* fix: markdown
1 parent dfcecf2 commit 73e3115

File tree

6 files changed

+67
-48
lines changed

6 files changed

+67
-48
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,7 @@ For example, it can:
5858
You can use the Apify MCP Server in two ways:
5959

6060
**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`.
61-
- `https://mcp.apify.com` (recommended) for streamable transport
62-
- `https://mcp.apify.com/sse` for legacy SSE transport
61+
- `https://mcp.apify.com` streamable transport
6362

6463
**Standard Input/Output (stdio)**: Ideal for local integrations and command-line tools like the Claude for Desktop client.
6564
- Set the MCP client server command to `npx @apify/actors-mcp-server` and the `APIFY_TOKEN` environment variable to your Apify API token.

src/tools/actor.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ EXAMPLES:
405405
}
406406
const toolsResponse = await client.listTools();
407407

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

411411
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:
420420
}
421421
const content = [
422422
// TODO: update result to say: this is result of info step, you must now call again with step=call and proper input
423-
{ type: 'text', text: `**Input Schema:**\n${JSON.stringify(details.inputSchema, null, 0)}` },
423+
{ type: 'text', text: `Input schema: \n${JSON.stringify(details.inputSchema, null, 0)}` },
424424
];
425425
/**
426426
* 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:
499499
if (errors && errors.length > 0) {
500500
return buildMCPResponse([
501501
`Input validation failed for Actor '${actorName}': ${errors.map((e) => e.message).join(', ')}`,
502-
`Input Schema:\n${JSON.stringify(actor.tool.inputSchema)}`,
502+
`Input schema:\n${JSON.stringify(actor.tool.inputSchema)}`,
503503
]);
504504
}
505505
}

src/tools/fetch-actor-details.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,23 @@ USAGE EXAMPLES:
4040
content: [{ type: 'text', text: `Actor information for '${parsed.actor}' was not found. Please check the Actor ID or name and ensure the Actor exists.` }],
4141
};
4242
}
43-
return {
44-
content: [
45-
{ type: 'text', text: `**Actor card**:\n${details.actorCard}` },
46-
{ type: 'text', text: `**README:**\n${details.readme}` },
47-
{ type: 'text', text: `**Input Schema:**\n${JSON.stringify(details.inputSchema, null, 0)}` },
48-
],
49-
};
43+
44+
const actorUrl = `https://apify.com/${details.actorInfo.username}/${details.actorInfo.name}`;
45+
// Add link to README title
46+
details.readme = details.readme.replace(/^# /, `# [README](${actorUrl}/readme): `);
47+
48+
const content = [
49+
{ type: 'text', text: `# Actor information\n${details.actorCard}` },
50+
{ type: 'text', text: `${details.readme}` },
51+
];
52+
53+
// Include input schema if it has properties
54+
if (details.inputSchema.properties || Object.keys(details.inputSchema.properties).length !== 0) {
55+
content.push({ type: 'text', text: `# [Input schema](${actorUrl}/input)\n\`\`\`json\n${JSON.stringify(details.inputSchema, null, 0)}\n\`\`\`` });
56+
}
57+
// Return the actor card, README, and input schema (if it has non-empty properties) as separate text blocks
58+
// This allows better formatting in the final output
59+
return { content };
5060
},
5161
} as InternalTool,
5262
};

src/tools/store_collection.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import zodToJsonSchema from 'zod-to-json-schema';
55
import { ApifyClient } from '../apify-client.js';
66
import { ACTOR_SEARCH_ABOVE_LIMIT, HelperTools } from '../const.js';
77
import type { ActorPricingModel, ExtendedActorStoreList, HelperTool, ToolEntry } from '../types.js';
8-
import { formatActorsListToActorCard } from '../utils/actor-card.js';
8+
import { formatActorToActorCard } from '../utils/actor-card.js';
99
import { ajv } from '../utils/ajv.js';
1010

1111
export async function searchActorsByKeywords(
@@ -99,14 +99,24 @@ USAGE EXAMPLES:
9999
parsed.offset,
100100
);
101101
actors = filterRentalActors(actors || [], userRentedActorIds || []).slice(0, parsed.limit);
102-
const actorCards = formatActorsListToActorCard(actors);
102+
const actorCards = actors.length === 0 ? [] : actors.map(formatActorToActorCard);
103+
104+
const actorsText = actorCards.length
105+
? actorCards.join('\n\n')
106+
: 'No Actors were found for the given search query. Please try different keywords or simplify your query.';
107+
103108
return {
104109
content: [
105110
{
106111
type: 'text',
107-
text: `**Search query:** ${parsed.search}\n\n`
108-
+ `**Number of Actors found:** ${actorCards.length}\n\n`
109-
+ `**Actor cards:**\n${actorCards.join('\n\n')}`,
112+
text: `
113+
# Search results:
114+
- **Search query:** ${parsed.search}
115+
- **Number of Actors found:** ${actorCards.length}
116+
117+
# Actors:
118+
119+
${actorsText}`,
110120
},
111121
],
112122
};

src/utils/actor-card.ts

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ function formatCategories(categories?: string[]): string[] {
2020
}
2121

2222
/**
23-
* Formats Actor details into an Actor card (Actor markdown representation).
23+
* Formats Actor details into an Actor card (Actor information in markdown).
2424
* @param actor - Actor information from the API
2525
* @returns Formatted actor card
2626
*/
@@ -42,14 +42,16 @@ export function formatActorToActorCard(
4242
}
4343

4444
const actorFullName = `${actor.username}/${actor.name}`;
45+
const actorUrl = `${APIFY_STORE_URL}/${actorFullName}`;
4546

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

5557
// Add stats - handle different stat structures
@@ -80,18 +82,18 @@ export function formatActorToActorCard(
8082
}
8183

8284
if (statsParts.length > 0) {
83-
markdownLines.push(`**Stats:** ${statsParts.join(', ')}`);
85+
markdownLines.push(`- **Stats:** ${statsParts.join(', ')}`);
8486
}
8587
}
8688

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

9294
// Add modification date if available
9395
if ('modifiedAt' in actor) {
94-
markdownLines.push(`**Last modified:** ${actor.modifiedAt.toISOString()}`);
96+
markdownLines.push(`- **Last modified:** ${actor.modifiedAt.toISOString()}`);
9597
}
9698

9799
// Add deprecation warning if applicable
@@ -100,18 +102,3 @@ export function formatActorToActorCard(
100102
}
101103
return markdownLines.join('\n');
102104
}
103-
104-
/**
105-
* Formats a list of Actors into Actor cards
106-
* @param actors - Array of Actor information
107-
* @returns Formatted markdown string
108-
*/
109-
export function formatActorsListToActorCard(actors: (Actor | ExtendedActorStoreList)[]): string[] {
110-
if (actors.length === 0) {
111-
return [];
112-
}
113-
return actors.map((actor) => {
114-
const card = formatActorToActorCard(actor);
115-
return `- ${card}`;
116-
});
117-
}

src/utils/pricing-info.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,24 @@ function convertMinutesToGreatestUnit(minutes: number): { value: number; unit: s
4141
return { value: Math.floor(minutes / (60 * 24)), unit: 'days' };
4242
}
4343

44+
/**
45+
* Formats the pay-per-event pricing information into a human-readable string.
46+
*
47+
* Example:
48+
* This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for the following events:
49+
* - Event title: Event description (Flat price: $X per event)
50+
* - MCP server startup: Initial fee for starting the Kiwi MCP Server Actor (Flat price: $0.1 per event)
51+
* - Flight search: Fee for searching flights using the Kiwi.com flight search engine (Flat price: $0.001 per event)
52+
*
53+
* For tiered pricing, the output is more complicated and the question is whether we want to simplify it in the future.
54+
* @param pricingPerEvent
55+
*/
56+
4457
function payPerEventPricingToString(pricingPerEvent: ExtendedPricingInfo['pricingPerEvent']): string {
45-
if (!pricingPerEvent || !pricingPerEvent.actorChargeEvents) return 'No event pricing information available.';
58+
if (!pricingPerEvent || !pricingPerEvent.actorChargeEvents) return 'Pricing information for events is not available.';
4659
const eventStrings: string[] = [];
4760
for (const event of Object.values(pricingPerEvent.actorChargeEvents)) {
48-
let eventStr = `- ${event.eventTitle}: ${event.eventDescription} `;
61+
let eventStr = `\t- **${event.eventTitle}**: ${event.eventDescription} `;
4962
if (typeof event.eventPriceUsd === 'number') {
5063
eventStr += `(Flat price: $${event.eventPriceUsd} per event)`;
5164
} else if (event.eventTieredPricingUsd) {
@@ -58,14 +71,14 @@ function payPerEventPricingToString(pricingPerEvent: ExtendedPricingInfo['pricin
5871
}
5972
eventStrings.push(eventStr);
6073
}
61-
return `This Actor charges per event as follows:\n${eventStrings.join('\n')}`;
74+
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')}`;
6275
}
6376

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

0 commit comments

Comments
 (0)