From 10f6d1ee70183f19b37b1568416a57b8b58dc10c Mon Sep 17 00:00:00 2001 From: MQ Date: Mon, 18 Aug 2025 12:41:44 +0200 Subject: [PATCH 1/2] feat: prepare for dockerhub integration, prepare dockerfile, add support for reading config from env vars for stdio --- Dockerfile | 15 +++-------- src/stdio.ts | 15 ++++------- tests/helpers.ts | 41 ++++++++++++++++++++--------- tests/integration/suite.ts | 53 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 33 deletions(-) diff --git a/Dockerfile b/Dockerfile index ba9de55e..84e93cd8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,5 @@ -# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile -# Stage 1: Build the TypeScript project -FROM node:18-alpine AS builder +# Stage 1: Build the project +FROM node:24-alpine AS builder # Set working directory WORKDIR /app @@ -17,7 +16,7 @@ COPY tsconfig.json ./ RUN npm run build # Stage 2: Set up the runtime environment -FROM node:18-alpine +FROM node:24-alpine # Set working directory WORKDIR /app @@ -29,11 +28,5 @@ COPY package.json package-lock.json ./ # Install production dependencies only RUN npm ci --omit=dev -# Expose any necessary ports (example: 3000) -EXPOSE 3000 - -# Set the environment variable for the Apify token -ENV APIFY_TOKEN= - # Set the entry point for the container -ENTRYPOINT ["node", "dist/main.js"] \ No newline at end of file +ENTRYPOINT ["node", "dist/stdio.js"] diff --git a/src/stdio.ts b/src/stdio.ts index 612f6643..6e4332e1 100644 --- a/src/stdio.ts +++ b/src/stdio.ts @@ -47,15 +47,16 @@ log.setLevel(log.LEVELS.ERROR); // Parse command line arguments using yargs const argv = yargs(hideBin(process.argv)) .usage('Usage: $0 [options]') + .env() .option('actors', { type: 'string', - describe: 'Comma-separated list of Actor full names to add to the server.', + describe: 'Comma-separated list of Actor full names to add to the server. Can also be set via ACTORS environment variable.', example: 'apify/google-search-scraper,apify/instagram-scraper', }) .option('enable-adding-actors', { type: 'boolean', default: true, - describe: 'Enable dynamically adding Actors as tools based on user requests.', + describe: 'Enable dynamically adding Actors as tools based on user requests. Can also be set via ENABLE_ADDING_ACTORS environment variable.', }) .option('enableActorAutoLoading', { type: 'boolean', @@ -65,17 +66,11 @@ const argv = yargs(hideBin(process.argv)) }) .options('tools', { type: 'string', - describe: `Comma-separated list of specific tool categories to enable. - -Available choices: ${Object.keys(toolCategories).join(', ')} - -Tool categories are as follows: + describe: `Comma-separated list of specific tool categories to enable. Can also be set via TOOLS environment variable.\n\nAvailable choices: ${Object.keys(toolCategories).join(', ')}\n\nTool categories are as follows: - docs: Search and fetch Apify documentation tools. - runs: Get Actor runs list, run details, and logs from a specific Actor run. - storage: Access datasets, key-value stores, and their records. -- preview: Experimental tools in preview mode. - -Note: Tools that enable you to search Actors from the Apify Store and get their details are always enabled by default. +- preview: Experimental tools in preview mode.\n\nNote: Tools that enable you to search Actors from the Apify Store and get their details are always enabled by default. `, example: 'docs,runs,storage', }) diff --git a/tests/helpers.ts b/tests/helpers.ts index fbf450b6..77223da3 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -11,6 +11,7 @@ export interface McpClientOptions { actors?: string[]; enableAddingActors?: boolean; tools?: ToolCategory[]; // Tool categories to include + useEnv?: boolean; // Use environment variables instead of command line arguments (stdio only) } export async function createMcpSseClient( @@ -97,24 +98,40 @@ export async function createMcpStdioClient( if (!process.env.APIFY_TOKEN) { throw new Error('APIFY_TOKEN environment variable is not set.'); } - const { actors, enableAddingActors, tools } = options || {}; + const { actors, enableAddingActors, tools, useEnv } = options || {}; const args = ['dist/stdio.js']; - if (actors) { - args.push('--actors', actors.join(',')); - } - if (enableAddingActors !== undefined) { - args.push('--enable-adding-actors', enableAddingActors.toString()); - } - if (tools && tools.length > 0) { - args.push('--tools', tools.join(',')); + const env: Record = { + APIFY_TOKEN: process.env.APIFY_TOKEN as string, + }; + + // Set environment variables instead of command line arguments when useEnv is true + if (useEnv) { + if (actors) { + env.ACTORS = actors.join(','); + } + if (enableAddingActors !== undefined) { + env.ENABLE_ADDING_ACTORS = enableAddingActors.toString(); + } + if (tools && tools.length > 0) { + env.TOOLS = tools.join(','); + } + } else { + // Use command line arguments as before + if (actors) { + args.push('--actors', actors.join(',')); + } + if (enableAddingActors !== undefined) { + args.push('--enable-adding-actors', enableAddingActors.toString()); + } + if (tools && tools.length > 0) { + args.push('--tools', tools.join(',')); + } } const transport = new StdioClientTransport({ command: 'node', args, - env: { - APIFY_TOKEN: process.env.APIFY_TOKEN as string, - }, + env, }); const client = new Client({ name: 'stdio-client', diff --git a/tests/integration/suite.ts b/tests/integration/suite.ts index d5a3975d..953762f0 100644 --- a/tests/integration/suite.ts +++ b/tests/integration/suite.ts @@ -493,5 +493,58 @@ export function createIntegrationTestsSuite( await (client.transport as StreamableHTTPClientTransport).terminateSession(); await client.close(); }); + + // Environment variable tests - only applicable to stdio transport + it.runIf(options.transport === 'stdio')('should load actors from ACTORS environment variable', async () => { + const actors = ['apify/python-example', 'apify/rag-web-browser']; + const client = await createClientFn({ actors, useEnv: true }); + const names = getToolNames(await client.listTools()); + expect(names.length).toEqual(defaultTools.length + actors.length + addRemoveTools.length); + expectToolNamesToContain(names, DEFAULT_TOOL_NAMES); + expectToolNamesToContain(names, actors.map((actor) => actorNameToToolName(actor))); + expectToolNamesToContain(names, addRemoveTools.map((tool) => tool.tool.name)); + + await client.close(); + }); + + it.runIf(options.transport === 'stdio')('should respect ENABLE_ADDING_ACTORS environment variable', async () => { + // Test with enableAddingActors = false via env var + const client = await createClientFn({ enableAddingActors: false, useEnv: true }); + const names = getToolNames(await client.listTools()); + expect(names.length).toEqual(defaultTools.length + defaults.actors.length); + + expectToolNamesToContain(names, DEFAULT_TOOL_NAMES); + expectToolNamesToContain(names, DEFAULT_ACTOR_NAMES); + await client.close(); + }); + + it.runIf(options.transport === 'stdio')('should load tool categories from TOOLS environment variable', async () => { + const categories = ['docs', 'runs'] as ToolCategory[]; + const client = await createClientFn({ tools: categories, useEnv: true }); + + const loadedTools = await client.listTools(); + const toolNames = getToolNames(loadedTools); + + const expectedTools = [ + ...toolCategories.docs, + ...toolCategories.runs, + ]; + const expectedToolNames = expectedTools.map((tool) => tool.tool.name); + + // Handle case where tools are enabled by default + const selectedCategoriesInDefault = categories.filter((key) => toolCategoriesEnabledByDefault.includes(key)); + const numberOfToolsFromCategoriesInDefault = selectedCategoriesInDefault + .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 + + (expectedTools.length - numberOfToolsFromCategoriesInDefault); + expect(toolNames.length).toEqual(numberOfToolsExpected); + for (const expectedToolName of expectedToolNames) { + expect(toolNames).toContain(expectedToolName); + } + + await client.close(); + }); }); } From 010a7f1688d70a0181bf2fb46ad9d0057b1cf436 Mon Sep 17 00:00:00 2001 From: MQ Date: Mon, 18 Aug 2025 12:46:54 +0200 Subject: [PATCH 2/2] format --- src/stdio.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/stdio.ts b/src/stdio.ts index 6e4332e1..e21d807f 100644 --- a/src/stdio.ts +++ b/src/stdio.ts @@ -66,11 +66,17 @@ const argv = yargs(hideBin(process.argv)) }) .options('tools', { type: 'string', - describe: `Comma-separated list of specific tool categories to enable. Can also be set via TOOLS environment variable.\n\nAvailable choices: ${Object.keys(toolCategories).join(', ')}\n\nTool categories are as follows: + describe: `Comma-separated list of specific tool categories to enable. Can also be set via TOOLS environment variable. + +Available choices: ${Object.keys(toolCategories).join(', ')} + +Tool categories are as follows: - docs: Search and fetch Apify documentation tools. - runs: Get Actor runs list, run details, and logs from a specific Actor run. - storage: Access datasets, key-value stores, and their records. -- preview: Experimental tools in preview mode.\n\nNote: Tools that enable you to search Actors from the Apify Store and get their details are always enabled by default. +- preview: Experimental tools in preview mode. + +Note: Tools that enable you to search Actors from the Apify Store and get their details are always enabled by default. `, example: 'docs,runs,storage', })