Skip to content

Commit c1f4ce1

Browse files
committed
feat: Return markdown from get-actor-details.ts
1 parent d78d12a commit c1f4ce1

File tree

3 files changed

+66
-73
lines changed

3 files changed

+66
-73
lines changed

README.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,12 +186,21 @@ Build the `actor-mcp-server` package:
186186
npm run build
187187
```
188188

189-
## Debugging
189+
## Start HTTP streamable MCP server
190190

191-
Since MCP servers operate over standard input/output (stdio), debugging can be challenging.
192-
For the best debugging experience, use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector).
191+
Run using Apify CLI:
193192

194-
You can launch the MCP Inspector via [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with this command:
193+
```bash
194+
export APIFY_TOKEN="your-apify-token"
195+
export APIFY_META_ORIGIN=STANDBY
196+
apify run -p
197+
```
198+
199+
Once the server is running, you can use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) to debug the server exposed at `http://localhost:3001`.
200+
201+
## Start standard input/output (stdio) MCP server
202+
203+
You can launch the MCP Inspector with this command:
195204

196205
```bash
197206
export APIFY_TOKEN="your-apify-token"

src/const.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,5 @@ export const ALGOLIA = {
7878
};
7979

8080
export const PROGRESS_NOTIFICATION_INTERVAL_MS = 5_000; // 5 seconds
81+
82+
export const APIFY_STORE_URL = 'https://apify.com';

src/tools/get-actor-details.ts

Lines changed: 51 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import type { Actor, Build } from 'apify-client';
12
import { z } from 'zod';
23
import zodToJsonSchema from 'zod-to-json-schema';
34

45
import { ApifyClient } from '../apify-client.js';
5-
import { HelperTools } from '../const.js';
6+
import { APIFY_STORE_URL, HelperTools } from '../const.js';
67
import type { ExtendedPricingInfo, IActorInputSchema, InternalTool, ToolEntry } from '../types.js';
78
import { ajv } from '../utils/ajv.js';
89
import { getCurrentPricingInfo, pricingInfoToString } from '../utils/pricing-info.js';
@@ -14,47 +15,34 @@ const getActorDetailsToolArgsSchema = z.object({
1415
.describe(`Actor ID or full name in the format "username/name", e.g., "apify/rag-web-browser".`),
1516
});
1617

17-
interface IGetActorDetailsToolResult {
18-
id: string;
19-
actorFullName: string;
20-
21-
isPublic: boolean;
22-
isDeprecated: boolean;
23-
createdAt: string;
24-
modifiedAt: string;
25-
26-
categories?: string[];
27-
description: string;
28-
readme: string;
29-
30-
inputSchema: IActorInputSchema;
31-
32-
pricingInfo: string; // We convert the pricing info into a string representation
33-
34-
usageStatistics: {
35-
totalUsers: {
36-
allTime: number;
37-
last7Days: number;
38-
last30Days: number;
39-
last90Days: number;
40-
};
41-
failedRunsInLast30Days: number | string; // string for 'unknown' case
42-
}
18+
// Helper function to format categories from uppercase with underscores to proper case
19+
function formatCategories(categories?: string[]): string[] {
20+
if (!categories) return [];
21+
22+
return categories.map((category) => {
23+
const formatted = category
24+
.toLowerCase()
25+
.split('_')
26+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
27+
.join(' ');
28+
// Special case for MCP server, AI, and SEO tools
29+
return formatted.replace('Mcp Server', 'MCP Server').replace('Ai', 'AI').replace('Seo', 'SEO');
30+
});
4331
}
4432

4533
export const getActorDetailsTool: ToolEntry = {
4634
type: 'internal',
4735
tool: {
4836
name: HelperTools.ACTOR_GET_DETAILS,
49-
description: `Retrieve information about an Actor by its ID or full name.\n`
50-
+ 'The Actor name is always composed of "username/name", for example, "apify/rag-web-browser".\n'
51-
+ 'This tool returns information about the Actor, including whether it is public or deprecated, '
52-
+ 'when it was created or modified, the categories in which the Actor is listed, a description, '
53-
+ 'a README (the Actor\'s documentation), the input schema, and usage statistics - such as how many users are using it '
54-
+ 'and the number of failed runs of the Actor.\n'
55-
+ 'For example, use this tool when a user wants to know more about a specific Actor or wants to use optional '
56-
+ 'or advanced parameters of the Actor that are not listed in the default Actor tool input schema - '
57-
+ 'so you know the details and how to pass them.',
37+
description: `Retrieve comprehensive details about an Actor using its ID or full name.\n`
38+
+ `This tool provides the Actor's title, description, URL, documentation (README), input schema, categories, pricing, and usage statistics.\n`
39+
+ `Specify the Actor name in the format "username/name" (e.g., "apify/rag-web-browser").\n`
40+
+ `The response is formatted in markdown and should be rendered as-is.\n`
41+
+ `USAGE:\n`
42+
+ `- Use when a user requests information about an Actor, such as its details, description, input schema, or documentation.\n`
43+
+ `EXAMPLES:\n`
44+
+ `- user_input: How to use apify/rag-web-browser\n`
45+
+ `- user_input: What is the input schema for apify/rag-web-browser`,
5846
inputSchema: zodToJsonSchema(getActorDetailsToolArgsSchema),
5947
ajvValidate: ajv.compile(zodToJsonSchema(getActorDetailsToolArgsSchema)),
6048
call: async (toolArgs) => {
@@ -63,7 +51,7 @@ export const getActorDetailsTool: ToolEntry = {
6351
const parsed = getActorDetailsToolArgsSchema.parse(args);
6452
const client = new ApifyClient({ token: apifyToken });
6553

66-
const [actorInfo, buildInfo] = await Promise.all([
54+
const [actorInfo, buildInfo]: [Actor | undefined, Build | undefined] = await Promise.all([
6755
client.actor(parsed.actor).get(),
6856
client.actor(parsed.actor).defaultBuild().then(async (build) => build.get()),
6957
]);
@@ -83,40 +71,34 @@ export const getActorDetailsTool: ToolEntry = {
8371

8472
const currentPricingInfo = getCurrentPricingInfo(actorInfo.pricingInfos || [], new Date());
8573

86-
const result: IGetActorDetailsToolResult = {
87-
id: actorInfo.id,
88-
actorFullName: `${actorInfo.username}/${actorInfo.name}`,
89-
90-
isPublic: actorInfo.isPublic,
91-
isDeprecated: actorInfo.isDeprecated || false,
92-
createdAt: actorInfo.createdAt.toISOString(),
93-
modifiedAt: actorInfo.modifiedAt.toISOString(),
94-
95-
categories: actorInfo.categories,
96-
description: actorInfo.description || 'No description provided.',
97-
readme: buildInfo.actorDefinition.readme || 'No README provided.',
98-
99-
inputSchema,
100-
101-
pricingInfo: pricingInfoToString(currentPricingInfo as (ExtendedPricingInfo | null)),
74+
// Format categories for display
75+
const formattedCategories = formatCategories(actorInfo.categories);
76+
77+
// Note: In the public API, we are missing maintainedByApify property, so we cannot use it here.
78+
// Note: Actor rating is not in public API, we need to add it (actorUtils.getActorReviewRatingNumber(actorId))
79+
const actorFullName = `${actorInfo.username}/${actorInfo.name}`;
80+
const markdownLines = [
81+
`Actor details (always present Actor information in this format, always include URL):\n`,
82+
`# [${actorInfo.title}](${APIFY_STORE_URL}/${actorFullName}) (${actorFullName})`,
83+
`**Developed by:** ${actorInfo.username} Maintained by ${actorInfo.username === 'apify' ? '(Apify)' : '(community)'}`,
84+
`**Description:** ${actorInfo.description || 'No description provided.'}`,
85+
`**Categories:** ${formattedCategories.length ? formattedCategories.join(', ') : 'Uncategorized'}`,
86+
`**Pricing:** ${pricingInfoToString(currentPricingInfo as (ExtendedPricingInfo | null))}`,
87+
`**Stats:** ${actorInfo.stats.totalUsers.toLocaleString()} total users, ${actorInfo.stats.totalUsers30Days.toLocaleString()} monthly users`,
88+
`Last modified: ${actorInfo.modifiedAt.toISOString()}`,
89+
];
90+
if (actorInfo.isDeprecated) {
91+
markdownLines.push('\n>This Actor is deprecated and may not be maintained anymore.');
92+
}
93+
const actorCard = markdownLines.join('\n');
10294

103-
usageStatistics: {
104-
totalUsers: {
105-
allTime: actorInfo.stats.totalUsers,
106-
last7Days: actorInfo.stats.totalUsers7Days,
107-
last30Days: actorInfo.stats.totalUsers30Days,
108-
last90Days: actorInfo.stats.totalUsers90Days,
109-
},
110-
failedRunsInLast30Days: (
111-
'publicActorRunStats30Days' in actorInfo.stats && 'FAILED' in (actorInfo.stats.publicActorRunStats30Days as object)
112-
) ? (actorInfo.stats.publicActorRunStats30Days as { FAILED: number }).FAILED : 'unknown',
113-
},
114-
};
11595
return {
116-
content: [{
117-
type: 'text',
118-
text: JSON.stringify(result),
119-
}],
96+
content: [
97+
{ type: 'text', text: actorCard },
98+
// LLM properly format Actor card, if README and input schema are separate text blocks
99+
{ type: 'text', text: `**README**:\n\n${buildInfo.actorDefinition.readme || 'No README provided.'}` },
100+
{ type: 'text', text: `**Input Schema**:\n\n${JSON.stringify(inputSchema, null, 0)}` },
101+
],
120102
};
121103
},
122104
} as InternalTool,

0 commit comments

Comments
 (0)