Skip to content

Commit e672ff2

Browse files
authored
feat: prompts (#187)
* add support for listing and getting prompts, * add rag search example prompt * update readme
1 parent 7c28562 commit e672ff2

File tree

6 files changed

+155
-5
lines changed

6 files changed

+155
-5
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,13 @@ Here are some special MCP operations and how the Apify MCP Server supports them:
152152

153153
For example, to enable all tools, use `npx @apify/actors-mcp-server --tools docs,runs,storage,preview` or `https://mcp.apify.com/?tools=docs,runs,storage,preview`.
154154

155-
### Prompt & Resources
155+
### Prompts
156156

157-
The server does not yet provide any resources or prompts.
157+
The server provides a set of predefined example prompts to help you get started interacting with Apify through MCP. For example, there is a `GetLatestNewsOnTopic` prompt that allows you to easily retrieve the latest news on a specific topic using the [RAG Web Browser](https://apify.com/apify/rag-web-browser) Actor.
158+
159+
### Resources
160+
161+
The server does not yet provide any resources.
158162

159163
### Debugging the NPM package
160164

src/mcp/server.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
CallToolRequestSchema,
1010
CallToolResultSchema,
1111
ErrorCode,
12+
GetPromptRequestSchema,
13+
ListPromptsRequestSchema,
1214
ListToolsRequestSchema,
1315
McpError,
1416
ServerNotificationSchema,
@@ -23,6 +25,7 @@ import {
2325
SERVER_NAME,
2426
SERVER_VERSION,
2527
} from '../const.js';
28+
import { prompts } from '../prompts/index.js';
2629
import { addRemoveTools, callActorGetDataset, defaultTools, getActorsAsTools, toolCategories } from '../tools/index.js';
2730
import { actorNameToToolName, decodeDotPropertyNames } from '../tools/utils.js';
2831
import type { ActorMcpTool, ActorTool, HelperTool, ToolEntry } from '../types.js';
@@ -61,13 +64,15 @@ export class ActorsMcpServer {
6164
{
6265
capabilities: {
6366
tools: { listChanged: true },
67+
prompts: { },
6468
logging: {},
6569
},
6670
},
6771
);
6872
this.tools = new Map();
6973
this.setupErrorHandling(setupSigintHandler);
7074
this.setupToolHandlers();
75+
this.setupPromptHandlers();
7176

7277
// Add default tools
7378
this.upsertTools(defaultTools);
@@ -334,6 +339,50 @@ export class ActorsMcpServer {
334339
}
335340
}
336341

342+
/**
343+
* Sets up MCP request handlers for prompts.
344+
*/
345+
private setupPromptHandlers(): void {
346+
/**
347+
* Handles the prompts/list request.
348+
*/
349+
this.server.setRequestHandler(ListPromptsRequestSchema, () => {
350+
return { prompts };
351+
});
352+
353+
/**
354+
* Handles the prompts/get request.
355+
*/
356+
this.server.setRequestHandler(GetPromptRequestSchema, (request) => {
357+
const { name, arguments: args } = request.params;
358+
const prompt = prompts.find((p) => p.name === name);
359+
if (!prompt) {
360+
throw new McpError(
361+
ErrorCode.InvalidParams,
362+
`Prompt ${name} not found. Available prompts: ${prompts.map((p) => p.name).join(', ')}`,
363+
);
364+
}
365+
if (!prompt.ajvValidate(args)) {
366+
throw new McpError(
367+
ErrorCode.InvalidParams,
368+
`Invalid arguments for prompt ${name}: args: ${JSON.stringify(args)} error: ${JSON.stringify(prompt.ajvValidate.errors)}`,
369+
);
370+
}
371+
return {
372+
description: prompt.description,
373+
messages: [
374+
{
375+
role: 'user',
376+
content: {
377+
type: 'text',
378+
text: prompt.render(args || {}),
379+
},
380+
},
381+
],
382+
};
383+
});
384+
}
385+
337386
private setupToolHandlers(): void {
338387
/**
339388
* Handles the request to list tools.

src/prompts/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { PromptBase } from '../types.js';
2+
import { latestNewsOnTopicPrompt } from './latest-news-on-topic.js';
3+
4+
/**
5+
* List of all enabled prompts.
6+
*/
7+
export const prompts: PromptBase[] = [
8+
latestNewsOnTopicPrompt,
9+
];
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { PromptArgument } from '@modelcontextprotocol/sdk/types.js';
2+
3+
import { fixedAjvCompile } from '../tools/utils.js';
4+
import type { PromptBase } from '../types.js';
5+
import { ajv } from '../utils/ajv.js';
6+
7+
/**
8+
* Prompt MCP arguments list.
9+
*/
10+
const args: PromptArgument[] = [
11+
{
12+
name: 'topic',
13+
description: 'The topic to retrieve the latest news on.',
14+
required: true,
15+
},
16+
{
17+
name: 'timespan',
18+
description: 'The timespan for which to retrieve news articles. Defaults to "7 days". For example "1 day", "3 days", "7 days", "1 month", etc.',
19+
required: false,
20+
},
21+
];
22+
23+
/**
24+
* Prompt AJV arguments schema for validation.
25+
*/
26+
const argsSchema = fixedAjvCompile(ajv, {
27+
type: 'object',
28+
properties: {
29+
...Object.fromEntries(args.map((arg) => [arg.name, {
30+
type: 'string',
31+
description: arg.description,
32+
default: arg.default,
33+
examples: arg.examples,
34+
}])),
35+
},
36+
required: [...args.filter((arg) => arg.required).map((arg) => arg.name)],
37+
});
38+
39+
/**
40+
* Actual prompt definition.
41+
*/
42+
export const latestNewsOnTopicPrompt: PromptBase = {
43+
name: 'GetLatestNewsOnTopic',
44+
description: 'This prompt retrieves the latest news articles on a selected topic.',
45+
arguments: args,
46+
ajvValidate: argsSchema,
47+
render: (data) => {
48+
const currentDateUtc = new Date().toISOString().split('T')[0];
49+
const timespan = data.timespan && data.timespan.trim() !== '' ? data.timespan : '7 days';
50+
return `I want you to use the RAG web browser to search the web for the latest news on the "${data.topic}" topic. Retrieve news from the last ${timespan}. The RAG web browser accepts a query parameter that supports all Google input, including filters and flags—be sure to use them to accomplish my goal. Today is ${currentDateUtc} UTC.`;
51+
},
52+
};

src/types.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
22
import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
3-
import type { Notification, Request } from '@modelcontextprotocol/sdk/types.js';
3+
import type { Notification, Prompt, Request } from '@modelcontextprotocol/sdk/types.js';
44
import type { ValidateFunction } from 'ajv';
55
import type { ActorDefaultRunOptions, ActorDefinition, ActorStoreList, PricingInfo } from 'apify-client';
66

@@ -275,3 +275,14 @@ export interface ApifyDocsSearchResult {
275275
/** Piece of content that matches the search query from Algolia */
276276
content: string;
277277
}
278+
279+
export type PromptBase = Prompt & {
280+
/**
281+
* AJV validation function for the prompt arguments.
282+
*/
283+
ajvValidate: ValidateFunction;
284+
/**
285+
* Function to render the prompt with given arguments
286+
*/
287+
render: (args: Record<string, string>) => string;
288+
};

tests/integration/suite.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/typ
44
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
55

66
import { defaults, HelperTools } from '../../src/const.js';
7+
import { latestNewsOnTopicPrompt } from '../../src/prompts/latest-news-on-topic.js';
78
import { addRemoveTools, defaultTools, toolCategories, toolCategoriesEnabledByDefault } from '../../src/tools/index.js';
89
import type { ISearchActorsResult } from '../../src/tools/store_collection.js';
910
import { actorNameToToolName } from '../../src/tools/utils.js';
@@ -436,8 +437,7 @@ export function createIntegrationTestsSuite(
436437
// Handle case where tools are enabled by default
437438
const selectedCategoriesInDefault = categories.filter((key) => toolCategoriesEnabledByDefault.includes(key));
438439
const numberOfToolsFromCategoriesInDefault = selectedCategoriesInDefault
439-
.map((key) => toolCategories[key])
440-
.flat().length;
440+
.flatMap((key) => toolCategories[key]).length;
441441

442442
const numberOfToolsExpected = defaultTools.length + defaults.actors.length + addRemoveTools.length
443443
// Tools from tool categories minus the ones already in default tools
@@ -450,6 +450,31 @@ export function createIntegrationTestsSuite(
450450
await client.close();
451451
});
452452

453+
it('should list all prompts', async () => {
454+
const client = await createClientFn();
455+
const prompts = await client.listPrompts();
456+
expect(prompts.prompts.length).toBeGreaterThan(0);
457+
await client.close();
458+
});
459+
460+
it('should be able to get prompt by name', async () => {
461+
const client = await createClientFn();
462+
463+
const topic = 'apify';
464+
const prompt = await client.getPrompt({
465+
name: latestNewsOnTopicPrompt.name,
466+
arguments: {
467+
topic,
468+
},
469+
});
470+
471+
const message = prompt.messages[0];
472+
expect(message).toBeDefined();
473+
expect(message.content.text).toContain(topic);
474+
475+
await client.close();
476+
});
477+
453478
// Session termination is only possible for streamable HTTP transport.
454479
it.runIf(options.transport === 'streamable-http')('should successfully terminate streamable session', async () => {
455480
const client = await createClientFn();

0 commit comments

Comments
 (0)