From 734433ac377730e98808f4c038c53cf4f3fc568e Mon Sep 17 00:00:00 2001 From: MQ Date: Wed, 23 Jul 2025 18:06:10 +0200 Subject: [PATCH 1/3] add support for listing and getting prompts, --- src/mcp/server.ts | 49 +++++++++++++++++++++++++++ src/prompts/index.ts | 9 +++++ src/prompts/latest-instagram-post.ts | 50 ++++++++++++++++++++++++++++ src/types.ts | 13 +++++++- tests/integration/suite.ts | 28 ++++++++++++++-- 5 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 src/prompts/index.ts create mode 100644 src/prompts/latest-instagram-post.ts diff --git a/src/mcp/server.ts b/src/mcp/server.ts index cc022826..fb9daad6 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -9,6 +9,8 @@ import { CallToolRequestSchema, CallToolResultSchema, ErrorCode, + GetPromptRequestSchema, + ListPromptsRequestSchema, ListToolsRequestSchema, McpError, ServerNotificationSchema, @@ -23,6 +25,7 @@ import { SERVER_NAME, SERVER_VERSION, } from '../const.js'; +import { prompts } from '../prompts/index.js'; import { addRemoveTools, callActorGetDataset, defaultTools, getActorsAsTools, toolCategories } from '../tools/index.js'; import { actorNameToToolName, decodeDotPropertyNames } from '../tools/utils.js'; import type { ActorMcpTool, ActorTool, HelperTool, ToolEntry } from '../types.js'; @@ -61,6 +64,7 @@ export class ActorsMcpServer { { capabilities: { tools: { listChanged: true }, + prompts: { }, logging: {}, }, }, @@ -68,6 +72,7 @@ export class ActorsMcpServer { this.tools = new Map(); this.setupErrorHandling(setupSigintHandler); this.setupToolHandlers(); + this.setupPromptHandlers(); // Add default tools this.upsertTools(defaultTools); @@ -334,6 +339,50 @@ export class ActorsMcpServer { } } + /** + * Sets up MCP request handlers for prompts. + */ + private setupPromptHandlers(): void { + /** + * Handles the prompts/list request. + */ + this.server.setRequestHandler(ListPromptsRequestSchema, () => { + return { prompts }; + }); + + /** + * Handles the prompts/get request. + */ + this.server.setRequestHandler(GetPromptRequestSchema, (request) => { + const { name, arguments: args } = request.params; + const prompt = prompts.find((p) => p.name === name); + if (!prompt) { + throw new McpError( + ErrorCode.InvalidParams, + `Prompt ${name} not found. Available prompts: ${prompts.map((p) => p.name).join(', ')}`, + ); + } + if (!prompt.ajvValidate(args)) { + throw new McpError( + ErrorCode.InvalidParams, + `Invalid arguments for prompt ${name}: args: ${JSON.stringify(args)} error: ${JSON.stringify(prompt.ajvValidate.errors)}`, + ); + } + return { + description: prompt.description, + messages: [ + { + role: 'user', + content: { + type: 'text', + text: prompt.render(args || {}), + }, + }, + ], + }; + }); + } + private setupToolHandlers(): void { /** * Handles the request to list tools. diff --git a/src/prompts/index.ts b/src/prompts/index.ts new file mode 100644 index 00000000..471f59b6 --- /dev/null +++ b/src/prompts/index.ts @@ -0,0 +1,9 @@ +import type { PromptBase } from '../types.js'; +import { latestInstagramPostPrompt } from './latest-instagram-post.js'; + +/** + * List of all enabled prompts. + */ +export const prompts: PromptBase[] = [ + latestInstagramPostPrompt, +]; diff --git a/src/prompts/latest-instagram-post.ts b/src/prompts/latest-instagram-post.ts new file mode 100644 index 00000000..d12647f4 --- /dev/null +++ b/src/prompts/latest-instagram-post.ts @@ -0,0 +1,50 @@ +import type { PromptArgument } from '@modelcontextprotocol/sdk/types.js'; + +import { fixedAjvCompile } from '../tools/utils.js'; +import type { PromptBase } from '../types.js'; +import { ajv } from '../utils/ajv.js'; + +/** + * Prompt MCP arguments list. + */ +const args: PromptArgument[] = [ + { + name: 'username', + description: 'The Instagram username of the account to retrieve the latest post from.', + required: true, + }, +]; + +/** + * Prompt AJV arguments schema for validation. + */ +const argsSchema = fixedAjvCompile(ajv, { + type: 'object', + properties: { + ...Object.fromEntries(args.map((arg) => [arg.name, { + type: 'string', + description: arg.description, + default: arg.default, + examples: arg.examples, + }])), + }, + required: [...args.filter((arg) => arg.required).map((arg) => arg.name)], +}); + +/** + * Actual prompt definition. + */ +export const latestInstagramPostPrompt: PromptBase = { + name: 'LatestInstagramPostPrompt', + description: 'This prompt retrieves the latest Instagram post of a selected instagram account.', + arguments: args, + ajvValidate: argsSchema, + render: ((data) => { + return `I want you to retrieve description, total number of likes and comments of the 1 latest Instagram post of the account with username "${data.username}". +To accomplish this you need to: +1) Add "apify/instagram-scraper" Actor to this session if not already present using the Actor add tool. +2) Get details about the Actor and its input schema using the get Actor details tool. +3) Run the Actor for the given username. +`; + }), +}; diff --git a/src/types.ts b/src/types.ts index 7a68cb17..d0114728 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,6 @@ import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'; -import type { Notification, Request } from '@modelcontextprotocol/sdk/types.js'; +import type { Notification, Prompt, Request } from '@modelcontextprotocol/sdk/types.js'; import type { ValidateFunction } from 'ajv'; import type { ActorDefaultRunOptions, ActorDefinition, ActorStoreList, PricingInfo } from 'apify-client'; @@ -275,3 +275,14 @@ export interface ApifyDocsSearchResult { /** Piece of content that matches the search query from Algolia */ content: string; } + +export type PromptBase = Prompt & { + /** + * AJV validation function for the prompt arguments. + */ + ajvValidate: ValidateFunction; + /** + * Function to render the prompt with given arguments + */ + render: (args: Record) => string; +}; diff --git a/tests/integration/suite.ts b/tests/integration/suite.ts index b5844875..6ad4b1de 100644 --- a/tests/integration/suite.ts +++ b/tests/integration/suite.ts @@ -436,8 +436,7 @@ export function createIntegrationTestsSuite( // Handle case where tools are enabled by default const selectedCategoriesInDefault = categories.filter((key) => toolCategoriesEnabledByDefault.includes(key)); const numberOfToolsFromCategoriesInDefault = selectedCategoriesInDefault - .map((key) => toolCategories[key]) - .flat().length; + .flatMap((key) => toolCategories[key]).length; const numberOfToolsExpected = defaultTools.length + defaults.actors.length + addRemoveTools.length // Tools from tool categories minus the ones already in default tools @@ -450,6 +449,31 @@ export function createIntegrationTestsSuite( await client.close(); }); + it('should list all prompts', async () => { + const client = await createClientFn(); + const prompts = await client.listPrompts(); + expect(prompts.prompts.length).toBeGreaterThan(0); + await client.close(); + }); + + it('should be able to get prompt by name', async () => { + const client = await createClientFn(); + + const username = 'apify'; + const prompt = await client.getPrompt({ + name: 'LatestInstagramPostPrompt', + arguments: { + username, + }, + }); + + const message = prompt.messages[0]; + expect(message).toBeDefined(); + expect(message.content.text).toContain(username); + + await client.close(); + }); + // Session termination is only possible for streamable HTTP transport. it.runIf(options.transport === 'streamable-http')('should successfully terminate streamable session', async () => { const client = await createClientFn(); From 5894ec4147acb7f81ecf719308df52bb854acafc Mon Sep 17 00:00:00 2001 From: MQ Date: Thu, 24 Jul 2025 13:35:35 +0200 Subject: [PATCH 2/3] add rag search example prompt --- src/prompts/index.ts | 4 +-- src/prompts/latest-instagram-post.ts | 50 -------------------------- src/prompts/latest-news-on-topic.ts | 52 ++++++++++++++++++++++++++++ tests/integration/suite.ts | 9 ++--- 4 files changed, 59 insertions(+), 56 deletions(-) delete mode 100644 src/prompts/latest-instagram-post.ts create mode 100644 src/prompts/latest-news-on-topic.ts diff --git a/src/prompts/index.ts b/src/prompts/index.ts index 471f59b6..5352abce 100644 --- a/src/prompts/index.ts +++ b/src/prompts/index.ts @@ -1,9 +1,9 @@ import type { PromptBase } from '../types.js'; -import { latestInstagramPostPrompt } from './latest-instagram-post.js'; +import { latestNewsOnTopicPrompt } from './latest-news-on-topic.js'; /** * List of all enabled prompts. */ export const prompts: PromptBase[] = [ - latestInstagramPostPrompt, + latestNewsOnTopicPrompt, ]; diff --git a/src/prompts/latest-instagram-post.ts b/src/prompts/latest-instagram-post.ts deleted file mode 100644 index d12647f4..00000000 --- a/src/prompts/latest-instagram-post.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { PromptArgument } from '@modelcontextprotocol/sdk/types.js'; - -import { fixedAjvCompile } from '../tools/utils.js'; -import type { PromptBase } from '../types.js'; -import { ajv } from '../utils/ajv.js'; - -/** - * Prompt MCP arguments list. - */ -const args: PromptArgument[] = [ - { - name: 'username', - description: 'The Instagram username of the account to retrieve the latest post from.', - required: true, - }, -]; - -/** - * Prompt AJV arguments schema for validation. - */ -const argsSchema = fixedAjvCompile(ajv, { - type: 'object', - properties: { - ...Object.fromEntries(args.map((arg) => [arg.name, { - type: 'string', - description: arg.description, - default: arg.default, - examples: arg.examples, - }])), - }, - required: [...args.filter((arg) => arg.required).map((arg) => arg.name)], -}); - -/** - * Actual prompt definition. - */ -export const latestInstagramPostPrompt: PromptBase = { - name: 'LatestInstagramPostPrompt', - description: 'This prompt retrieves the latest Instagram post of a selected instagram account.', - arguments: args, - ajvValidate: argsSchema, - render: ((data) => { - return `I want you to retrieve description, total number of likes and comments of the 1 latest Instagram post of the account with username "${data.username}". -To accomplish this you need to: -1) Add "apify/instagram-scraper" Actor to this session if not already present using the Actor add tool. -2) Get details about the Actor and its input schema using the get Actor details tool. -3) Run the Actor for the given username. -`; - }), -}; diff --git a/src/prompts/latest-news-on-topic.ts b/src/prompts/latest-news-on-topic.ts new file mode 100644 index 00000000..aeea3f70 --- /dev/null +++ b/src/prompts/latest-news-on-topic.ts @@ -0,0 +1,52 @@ +import type { PromptArgument } from '@modelcontextprotocol/sdk/types.js'; + +import { fixedAjvCompile } from '../tools/utils.js'; +import type { PromptBase } from '../types.js'; +import { ajv } from '../utils/ajv.js'; + +/** + * Prompt MCP arguments list. + */ +const args: PromptArgument[] = [ + { + name: 'topic', + description: 'The topic to retrieve the latest news on.', + required: true, + }, + { + name: 'timespan', + description: 'The timespan for which to retrieve news articles. Defaults to "7 days". For example "1 day", "3 days", "7 days", "1 month", etc.', + required: false, + }, +]; + +/** + * Prompt AJV arguments schema for validation. + */ +const argsSchema = fixedAjvCompile(ajv, { + type: 'object', + properties: { + ...Object.fromEntries(args.map((arg) => [arg.name, { + type: 'string', + description: arg.description, + default: arg.default, + examples: arg.examples, + }])), + }, + required: [...args.filter((arg) => arg.required).map((arg) => arg.name)], +}); + +/** + * Actual prompt definition. + */ +export const latestNewsOnTopicPrompt: PromptBase = { + name: 'GetLatestNewsOnTopic', + description: 'This prompt retrieves the latest news articles on a selected topic.', + arguments: args, + ajvValidate: argsSchema, + render: (data) => { + const currentDateUtc = new Date().toISOString().split('T')[0]; + const timespan = data.timespan && data.timespan.trim() !== '' ? data.timespan : '7 days'; + 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.`; + }, +}; diff --git a/tests/integration/suite.ts b/tests/integration/suite.ts index 6ad4b1de..d03c80d2 100644 --- a/tests/integration/suite.ts +++ b/tests/integration/suite.ts @@ -4,6 +4,7 @@ import { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/typ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { defaults, HelperTools } from '../../src/const.js'; +import { latestNewsOnTopicPrompt } from '../../src/prompts/latest-news-on-topic.js'; import { addRemoveTools, defaultTools, toolCategories, toolCategoriesEnabledByDefault } from '../../src/tools/index.js'; import type { ISearchActorsResult } from '../../src/tools/store_collection.js'; import { actorNameToToolName } from '../../src/tools/utils.js'; @@ -459,17 +460,17 @@ export function createIntegrationTestsSuite( it('should be able to get prompt by name', async () => { const client = await createClientFn(); - const username = 'apify'; + const topic = 'apify'; const prompt = await client.getPrompt({ - name: 'LatestInstagramPostPrompt', + name: latestNewsOnTopicPrompt.name, arguments: { - username, + topic, }, }); const message = prompt.messages[0]; expect(message).toBeDefined(); - expect(message.content.text).toContain(username); + expect(message.content.text).toContain(topic); await client.close(); }); From 311722bb24ce1bab39c174205c20a1dcc48c36a0 Mon Sep 17 00:00:00 2001 From: MQ Date: Thu, 24 Jul 2025 16:59:58 +0200 Subject: [PATCH 3/3] update readme --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0716f0dd..e08d5512 100644 --- a/README.md +++ b/README.md @@ -152,9 +152,13 @@ Here are some special MCP operations and how the Apify MCP Server supports them: 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`. -### Prompt & Resources +### Prompts -The server does not yet provide any resources or prompts. +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. + +### Resources + +The server does not yet provide any resources. ### Debugging the NPM package