Skip to content

Commit bfd9016

Browse files
authored
feat: Return Actor card in markdown (#195)
* feat: Return markdown from get-actor-details.ts * feat: Return markdown search-actors tools
1 parent 56dcfb9 commit bfd9016

File tree

11 files changed

+205
-212
lines changed

11 files changed

+205
-212
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/actor.ts

Lines changed: 7 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { getActorMCPServerPath, getActorMCPServerURL } from '../mcp/actors.js';
1616
import { connectMCPClient } from '../mcp/client.js';
1717
import { getMCPServerTools } from '../mcp/proxy.js';
1818
import { actorDefinitionPrunedCache } from '../state.js';
19-
import type { ActorDefinitionStorage, ActorInfo, InternalTool, ToolEntry } from '../types.js';
19+
import type { ActorDefinitionStorage, ActorInfo, ToolEntry } from '../types.js';
2020
import { getActorDefinitionStorageFieldNames } from '../utils/actor.js';
2121
import { getValuesByDotKeys } from '../utils/generic.js';
2222
import type { ProgressTracker } from '../utils/progress.js';
@@ -257,41 +257,6 @@ export async function getActorsAsTools(
257257
return [...normalTools, ...mcpServerTools];
258258
}
259259

260-
const getActorArgs = z.object({
261-
actorId: z.string()
262-
.min(1)
263-
.describe('Actor ID or a tilde-separated owner\'s username and Actor name.'),
264-
});
265-
266-
/**
267-
* https://docs.apify.com/api/v2/act-get
268-
*/
269-
export const getActor: ToolEntry = {
270-
type: 'internal',
271-
tool: {
272-
name: HelperTools.ACTOR_GET,
273-
actorFullName: HelperTools.ACTOR_GET,
274-
description: 'Gets an object that contains all the details about a specific Actor.'
275-
+ 'Actor basic information (ID, name, owner, description)'
276-
+ 'Statistics (number of runs, users, etc.)'
277-
+ 'Available versions, and configuration details'
278-
+ 'Use Actor ID or Actor full name, separated by tilde username~name.',
279-
inputSchema: zodToJsonSchema(getActorArgs),
280-
ajvValidate: ajv.compile(zodToJsonSchema(getActorArgs)),
281-
call: async (toolArgs) => {
282-
const { args, apifyToken } = toolArgs;
283-
const { actorId } = getActorArgs.parse(args);
284-
const client = new ApifyClient({ token: apifyToken });
285-
// Get Actor - contains a lot of irrelevant information
286-
const actor = await client.actor(actorId).get();
287-
if (!actor) {
288-
return { content: [{ type: 'text', text: `Actor '${actorId}' not found.` }] };
289-
}
290-
return { content: [{ type: 'text', text: JSON.stringify(actor) }] };
291-
},
292-
} as InternalTool,
293-
};
294-
295260
const callActorArgs = z.object({
296261
actor: z.string()
297262
.describe('The name of the Actor to call. For example, "apify/instagram-scraper".'),
@@ -330,12 +295,12 @@ export const callActor: ToolEntry = {
330295
return {
331296
content: [{
332297
type: 'text',
333-
text: `Actor '${actorName}' is not added. ${toolsText}
334-
To use this MCP server, specify the actors with the parameter, for example:
335-
?actors=apify/instagram-scraper,apify/website-content-crawler
336-
or with the CLI:
337-
--actors "apify/instagram-scraper,apify/website-content-crawler"
338-
You can only use actors that are included in the list; actors not in the list cannot be used.`,
298+
text: `Actor '${actorName}' is not added. ${toolsText}\n`
299+
+ 'To use this MCP server, specify the actors with the parameter, for example:\n'
300+
+ '?actors=apify/instagram-scraper,apify/website-content-crawler\n'
301+
+ 'or with the CLI:\n'
302+
+ '--actors "apify/instagram-scraper,apify/website-content-crawler"\n'
303+
+ 'You can only use actors that are included in the list; actors not in the list cannot be used.',
339304
}],
340305
};
341306
}

src/tools/get-actor-details.ts

Lines changed: 20 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
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';
56
import { HelperTools } from '../const.js';
6-
import type { ExtendedPricingInfo, IActorInputSchema, InternalTool, ToolEntry } from '../types.js';
7+
import type { IActorInputSchema, InternalTool, ToolEntry } from '../types.js';
8+
import { formatActorToActorCard } from '../utils/actor-card.js';
79
import { ajv } from '../utils/ajv.js';
8-
import { getCurrentPricingInfo, pricingInfoToString } from '../utils/pricing-info.js';
910
import { filterSchemaProperties, shortenProperties } from './utils.js';
1011

1112
const getActorDetailsToolArgsSchema = z.object({
@@ -14,42 +15,19 @@ 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-
}
43-
}
44-
4518
export const getActorDetailsTool: ToolEntry = {
4619
type: 'internal',
4720
tool: {
4821
name: HelperTools.ACTOR_GET_DETAILS,
49-
description: `Retrieve information about an Actor by its ID or full name.
50-
The Actor name is always composed of "username/name", for example, "apify/rag-web-browser".
51-
This tool returns information about the Actor, including whether it is public or deprecated, when it was created or modified, the categories in which the Actor is listed, a description, a README (the Actor's documentation), the input schema, and usage statistics - such as how many users are using it and the number of failed runs of the Actor.
52-
For example, use this tool when a user wants to know more about a specific Actor or wants to use optional or advanced parameters of the Actor that are not listed in the default Actor tool input schema - so you know the details and how to pass them.`,
22+
description: `Get detailed information about an Actor by its ID or full name.\n`
23+
+ `This tool returns title, description, URL, README (Actor's documentation), input schema, and usage statistics. \n`
24+
+ `The Actor name is always composed of "username/name", for example, "apify/rag-web-browser".\n`
25+
+ `Present Actor information in user-friendly format as an Actor card.\n`
26+
+ `USAGE:\n`
27+
+ `- Use when user asks about an Actor its details, description, input schema, etc.\n`
28+
+ `EXAMPLES:\n`
29+
+ `- user_input: How to use apify/rag-web-browser\n`
30+
+ `- user_input: What is the input schema for apify/rag-web-browser`,
5331
inputSchema: zodToJsonSchema(getActorDetailsToolArgsSchema),
5432
ajvValidate: ajv.compile(zodToJsonSchema(getActorDetailsToolArgsSchema)),
5533
call: async (toolArgs) => {
@@ -58,7 +36,7 @@ For example, use this tool when a user wants to know more about a specific Actor
5836
const parsed = getActorDetailsToolArgsSchema.parse(args);
5937
const client = new ApifyClient({ token: apifyToken });
6038

61-
const [actorInfo, buildInfo] = await Promise.all([
39+
const [actorInfo, buildInfo]: [Actor | undefined, Build | undefined] = await Promise.all([
6240
client.actor(parsed.actor).get(),
6341
client.actor(parsed.actor).defaultBuild().then(async (build) => build.get()),
6442
]);
@@ -76,42 +54,15 @@ For example, use this tool when a user wants to know more about a specific Actor
7654
inputSchema.properties = filterSchemaProperties(inputSchema.properties);
7755
inputSchema.properties = shortenProperties(inputSchema.properties);
7856

79-
const currentPricingInfo = getCurrentPricingInfo(actorInfo.pricingInfos || [], new Date());
57+
// Use the actor formatter to get the main actor details
58+
const actorCard = formatActorToActorCard(actorInfo);
8059

81-
const result: IGetActorDetailsToolResult = {
82-
id: actorInfo.id,
83-
actorFullName: `${actorInfo.username}/${actorInfo.name}`,
84-
85-
isPublic: actorInfo.isPublic,
86-
isDeprecated: actorInfo.isDeprecated || false,
87-
createdAt: actorInfo.createdAt.toISOString(),
88-
modifiedAt: actorInfo.modifiedAt.toISOString(),
89-
90-
categories: actorInfo.categories,
91-
description: actorInfo.description || 'No description provided.',
92-
readme: buildInfo.actorDefinition.readme || 'No README provided.',
93-
94-
inputSchema,
95-
96-
pricingInfo: pricingInfoToString(currentPricingInfo as (ExtendedPricingInfo | null)),
97-
98-
usageStatistics: {
99-
totalUsers: {
100-
allTime: actorInfo.stats.totalUsers,
101-
last7Days: actorInfo.stats.totalUsers7Days,
102-
last30Days: actorInfo.stats.totalUsers30Days,
103-
last90Days: actorInfo.stats.totalUsers90Days,
104-
},
105-
failedRunsInLast30Days: (
106-
'publicActorRunStats30Days' in actorInfo.stats && 'FAILED' in (actorInfo.stats.publicActorRunStats30Days as object)
107-
) ? (actorInfo.stats.publicActorRunStats30Days as { FAILED: number }).FAILED : 'unknown',
108-
},
109-
};
11060
return {
111-
content: [{
112-
type: 'text',
113-
text: JSON.stringify(result),
114-
}],
61+
content: [
62+
{ type: 'text', text: `**Actor card**:\n${actorCard}` },
63+
{ type: 'text', text: `**README:**\n${buildInfo.actorDefinition.readme || 'No README provided.'}` },
64+
{ type: 'text', text: `**Input Schema:**\n${JSON.stringify(inputSchema, null, 0)}` },
65+
],
11566
};
11667
},
11768
} as InternalTool,

src/tools/helpers.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,12 @@ export const addTool: ToolEntry = {
3939
type: 'internal',
4040
tool: {
4141
name: HelperTools.ACTOR_ADD,
42-
description:
43-
`Add an Actor or MCP server to the available tools of the Apify MCP server.
44-
A tool is an Actor or MCP server that can be called by the user.
45-
Do not execute the tool, only add it and list it in the available tools.
46-
For example, when a user wants to scrape a website, first search for relevant Actors
47-
using ${HelperTools.STORE_SEARCH} tool, and once the user selects one they want to use,
48-
add it as a tool to the Apify MCP server.`,
42+
description: `Add an Actor or MCP server to the available tools of the Apify MCP server.\n`
43+
+ 'A tool is an Actor or MCP server that can be called by the user.\n'
44+
+ 'Do not execute the tool, only add it and list it in the available tools.\n'
45+
+ 'For example, when a user wants to scrape a website, first search for relevant Actors\n'
46+
+ `using ${HelperTools.STORE_SEARCH} tool, and once the user selects one they want to use,\n`
47+
+ 'add it as a tool to the Apify MCP server.',
4948
inputSchema: zodToJsonSchema(addToolArgsSchema),
5049
ajvValidate: ajv.compile(zodToJsonSchema(addToolArgsSchema)),
5150
// TODO: I don't like that we are passing apifyMcpServer and mcpServer to the tool
@@ -120,9 +119,10 @@ export const helpTool: ToolEntry = {
120119
type: 'internal',
121120
tool: {
122121
name: HelperTools.APIFY_MCP_HELP_TOOL,
123-
description: `Helper tool to get information on how to use and troubleshoot the Apify MCP server.
124-
This tool always returns the same help message with information about the server and how to use it.
125-
ALWAYS CALL THIS TOOL AT THE BEGINNING OF THE CONVERSATION SO THAT YOU HAVE INFORMATION ABOUT THE APIFY MCP SERVER IN CONTEXT, OR WHEN YOU ENCOUNTER ANY ISSUES WITH THE MCP SERVER OR ITS TOOLS.`,
122+
description: `Helper tool to get information on how to use and troubleshoot the Apify MCP server.\n`
123+
+ 'This tool always returns the same help message with information about the server and how to use it.\n'
124+
+ 'ALWAYS CALL THIS TOOL AT THE BEGINNING OF THE CONVERSATION SO THAT YOU HAVE INFORMATION ABOUT THE APIFY MCP SERVER IN CONTEXT, '
125+
+ 'OR WHEN YOU ENCOUNTER ANY ISSUES WITH THE MCP SERVER OR ITS TOOLS.',
126126
inputSchema: zodToJsonSchema(helpToolArgsSchema),
127127
ajvValidate: ajv.compile(zodToJsonSchema(helpToolArgsSchema)),
128128
call: async () => {

src/tools/run_collection.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ export const getUserRunsList: ToolEntry = {
3232
tool: {
3333
name: HelperTools.ACTOR_RUN_LIST_GET,
3434
actorFullName: HelperTools.ACTOR_RUN_LIST_GET,
35-
description: `Gets a paginated list of Actor runs with run details, datasetId, and keyValueStoreId.
36-
Filter by status: READY (not allocated), RUNNING (executing), SUCCEEDED (finished), FAILED (failed),
37-
TIMING-OUT (timing out), TIMED-OUT (timed out), ABORTING (being aborted), ABORTED (aborted).`,
35+
description: `Gets a paginated list of Actor runs with run details, datasetId, and keyValueStoreId.\n`
36+
+ 'Filter by status: READY (not allocated), RUNNING (executing), SUCCEEDED (finished), FAILED (failed),\n'
37+
+ 'TIMING-OUT (timing out), TIMED-OUT (timed out), ABORTING (being aborted), ABORTED (aborted).',
3838
inputSchema: zodToJsonSchema(getUserRunsListArgs),
3939
ajvValidate: ajv.compile(zodToJsonSchema(getUserRunsListArgs)),
4040
call: async (toolArgs) => {

src/tools/search-apify-docs.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@ export const searchApifyDocsTool: ToolEntry = {
3030
type: 'internal',
3131
tool: {
3232
name: HelperTools.DOCS_SEARCH,
33-
description: `Apify documentation search tool. This tool allows you to search the Apify documentation using Algolia's full-text search.
34-
You can use it to find relevant documentation pages based on keywords. The results will include the URL of the documentation page, a fragment identifier (if available), and a limited piece of content that matches the search query. You can then fetch the full content of the document using the ${HelperTools.DOCS_FETCH} tool by providing the URL.
35-
Use this tool when a user asks for help with Apify documentation or when you need to find relevant documentation pages based on keywords. For example, when a user wants to build an Apify Actor, you can search "How to build Actors" to find relevant guidance.`,
33+
description: `Apify documentation search tool. This tool allows you to search the Apify documentation using Algolia's full-text search.\n`
34+
+ 'You can use it to find relevant documentation pages based on keywords. The results will include the URL of the documentation page, '
35+
+ 'a fragment identifier (if available), and a limited piece of content that matches the search query. '
36+
+ `You can then fetch the full content of the document using the ${HelperTools.DOCS_FETCH} tool by providing the URL.\n`
37+
+ 'Use this tool when a user asks for help with Apify documentation or when you need to find relevant documentation pages based on keywords. '
38+
+ 'For example, when a user wants to build an Apify Actor, you can search "How to build Actors" to find relevant guidance.',
3639
args: searchApifyDocsToolArgsSchema,
3740
inputSchema: zodToJsonSchema(searchApifyDocsToolArgsSchema),
3841
ajvValidate: ajv.compile(zodToJsonSchema(searchApifyDocsToolArgsSchema)),

0 commit comments

Comments
 (0)