From 07bb3aeb69b28a1ee25715393ad08af073973372 Mon Sep 17 00:00:00 2001 From: Jiri Spilka Date: Fri, 22 Aug 2025 14:17:23 +0200 Subject: [PATCH 01/11] feat: Add deployment to smithery --- Dockerfile | 21 +++++++++-------- package.json | 1 + smithery.yaml | 18 +-------------- src/index.ts | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/stdio.ts | 18 ++------------- src/types.ts | 29 ++++++++++++++++++++++++ 6 files changed, 105 insertions(+), 44 deletions(-) diff --git a/Dockerfile b/Dockerfile index 84e93cd8..e15c669a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,32 +1,31 @@ -# Stage 1: Build the project -FROM node:24-alpine AS builder +FROM node:22.12-alpine AS builder -# Set working directory WORKDIR /app # Copy package files and install dependencies COPY package.json package-lock.json ./ -RUN npm install -# Copy source files +# Install all dependencies (including devDependencies for build) +RUN npm ci --ignore-scripts + COPY src ./src COPY tsconfig.json ./ -# Build the project RUN npm run build -# Stage 2: Set up the runtime environment -FROM node:24-alpine +FROM node:22-alpine AS release -# Set working directory WORKDIR /app # Copy only the necessary files from the build stage COPY --from=builder /app/dist ./dist COPY package.json package-lock.json ./ +ENV NODE_ENV=production +ENV APIFY_TOKEN=your-api-key-here + # Install production dependencies only -RUN npm ci --omit=dev +RUN npm ci --ignore-scripts --omit=dev # Set the entry point for the container -ENTRYPOINT ["node", "dist/stdio.js"] +ENTRYPOINT ["node", "dist/index.js"] diff --git a/package.json b/package.json index 095a1be2..5966df26 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "node": ">=18.0.0" }, "main": "dist/index.js", + "module": "./src/index.ts", "exports": { ".": "./dist/index.js", "./internals": "./dist/index-internals.js", diff --git a/smithery.yaml b/smithery.yaml index 01616594..ebede504 100644 --- a/smithery.yaml +++ b/smithery.yaml @@ -1,17 +1 @@ -# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml - -startCommand: - type: stdio - configSchema: - # JSON Schema defining the configuration options for the MCP. - type: object - required: - - apifyToken - properties: - apifyToken: - type: string - description: The API token for accessing Apify's services. - commandFunction: - # A function that produces the CLI command to start the MCP on stdio. - |- - (config) => ({ command: 'node', args: ['dist/main.js'], env: { APIFY_TOKEN: config.apifyToken } }) \ No newline at end of file +runtime: typescript \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 555d3645..ea26c459 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,70 @@ +#!/usr/bin/env node /* This file provides essential functions and tools for MCP servers, serving as a library. The ActorsMcpServer should be the only class exported from the package + + Also, it serves as the main entry point for smithery deployment. */ +import type { z } from 'zod'; import { ActorsMcpServer } from './mcp/server.js'; +import type { Input, ToolCategory } from './types'; +import { serverConfigSchemaSmithery as configSchema } from './types.js'; +import { loadToolsFromInput } from './utils/tools-loader.js'; + +// Export the config schema for Smithery. The export must be named configSchema +export { configSchema }; export { ActorsMcpServer }; + +/** + * Main entrypoint for Smithery deployment, do not change signature of this function. + * @param param0 + * @returns + */ +// eslint-disable-next-line import/no-default-export +export default function ({ config: _config }: { config: z.infer }) { + try { + const apifyToken = _config.apifyToken || process.env.APIFY_TOKEN || ''; + const enableAddingActors = _config.enableAddingActors ?? true; + const actors = _config.actors || ''; + const actorList = actors ? actors.split(',').map((a: string) => a.trim()) : []; + const toolCategoryKeys = _config.tools ? _config.tools.split(',').map((t: string) => t.trim()) : []; + + // Validate environment + if (!apifyToken) { + // eslint-disable-next-line no-console + console.warn('APIFY_TOKEN is required but not set in the environment variables or config. Some tools may not work properly.'); + } else { + process.env.APIFY_TOKEN = apifyToken; // Ensure token is set in the environment + } + + const server = new ActorsMcpServer({ enableAddingActors, enableDefaultActors: false }); + + const input: Input = { + actors: actorList.length ? actorList : [], + enableAddingActors, + tools: toolCategoryKeys as ToolCategory[], + }; + + // NOTE: This is a workaround for Smithery's requirement of a synchronous function + // We load tools asynchronously and attach the promise to the server + // However, this approach is NOT 100% reliable - the external library may still + // try to use the server before tools are fully loaded + loadToolsFromInput(input, apifyToken, actorList.length === 0) + .then((tools) => { + server.upsertTools(tools); + return true; + }) + .catch((error) => { + // eslint-disable-next-line no-console + console.error('Failed to load tools:', error); + return false; + }); + return server.server; + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + throw e; + } +} diff --git a/src/stdio.ts b/src/stdio.ts index e21d807f..00a2ea5b 100644 --- a/src/stdio.ts +++ b/src/stdio.ts @@ -24,23 +24,9 @@ import log from '@apify/log'; import { ActorsMcpServer } from './mcp/server.js'; import { toolCategories } from './tools/index.js'; -import type { Input, ToolCategory } from './types.js'; +import type { Input, ServerConfigCli, ToolCategory } from './types.js'; import { loadToolsFromInput } from './utils/tools-loader.js'; -// Keeping this interface here and not types.ts since -// it is only relevant to the CLI/STDIO transport in this file -/** - * Interface for command line arguments - */ -interface CliArgs { - actors?: string; - enableAddingActors: boolean; - /** @deprecated */ - enableActorAutoLoading: boolean; - /** Tool categories to include */ - tools?: string; -} - // Configure logging, set to ERROR log.setLevel(log.LEVELS.ERROR); @@ -88,7 +74,7 @@ Note: Tools that enable you to search Actors from the Apify Store and get their + ' and set the environment variable `APIFY_TOKEN` to your Apify API token.\n', ) .epilogue('For more information, visit https://mcp.apify.com or https://github.com/apify/actors-mcp-server') - .parseSync() as CliArgs; + .parseSync() as ServerConfigCli; const enableAddingActors = argv.enableAddingActors && argv.enableActorAutoLoading; const actors = argv.actors as string || ''; diff --git a/src/types.ts b/src/types.ts index 497cd515..d2a70ca7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,12 +3,41 @@ import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/proto import type { Notification, Prompt, Request } from '@modelcontextprotocol/sdk/types.js'; import type { ValidateFunction } from 'ajv'; import type { ActorDefaultRunOptions, ActorDefinition, ActorStoreList, PricingInfo } from 'apify-client'; +import { z } from 'zod'; import type { ACTOR_PRICING_MODEL } from './const.js'; import type { ActorsMcpServer } from './mcp/server.js'; import type { toolCategories } from './tools/index.js'; import type { ProgressTracker } from './utils/progress.js'; +export const serverConfigSchemaCli = z.object({ + actors: z + .string() + .optional() + .describe('Comma-separated list of Actor full names to add to the server'), + enableAddingActors: z + .boolean() + .default(true) + .describe('Enable dynamically adding Actors as tools based on user requests'), + tools: z + .string() + .optional() + .describe('Comma-separated list of specific tool categories to enable (docs,runs,storage,preview)'), + /** @deprecated */ + enableActorAutoLoading: z + .boolean() + .default(true) + .describe('Deprecated: use enable-adding-actors instead.'), +}); + +export const serverConfigSchemaSmithery = serverConfigSchemaCli.extend({ + apifyToken: z + .string() + .describe('Apify token, learn more: https://docs.apify.com/platform/integrations/api#api-token'), +}); + +export type ServerConfigCli = z.infer; + export interface ISchemaProperties { type: string; From 8ffbc5f43a4480df0a79efe673829d8bba124a26 Mon Sep 17 00:00:00 2001 From: Jiri Spilka Date: Fri, 22 Aug 2025 15:48:40 +0200 Subject: [PATCH 02/11] fix: node:24 --- Dockerfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index e15c669a..799417fa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22.12-alpine AS builder +FROM node:24-alpine AS builder WORKDIR /app @@ -13,7 +13,7 @@ COPY tsconfig.json ./ RUN npm run build -FROM node:22-alpine AS release +FROM node:24-alpine AS release WORKDIR /app @@ -22,7 +22,6 @@ COPY --from=builder /app/dist ./dist COPY package.json package-lock.json ./ ENV NODE_ENV=production -ENV APIFY_TOKEN=your-api-key-here # Install production dependencies only RUN npm ci --ignore-scripts --omit=dev From cca4a4661ba488b8389366b8c3e00e79e1b3f979 Mon Sep 17 00:00:00 2001 From: Jiri Spilka Date: Fri, 22 Aug 2025 15:55:12 +0200 Subject: [PATCH 03/11] fix: I've become smithery master :D --- Dockerfile | 18 +++++++------ Dockerfile.smithery | 30 +++++++++++++++++++++ smithery.yaml | 7 ++++- src/index.ts | 62 ------------------------------------------ src/smithery.ts | 66 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 112 insertions(+), 71 deletions(-) create mode 100644 Dockerfile.smithery create mode 100644 src/smithery.ts diff --git a/Dockerfile b/Dockerfile index 799417fa..84e93cd8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,30 +1,32 @@ +# Stage 1: Build the project FROM node:24-alpine AS builder +# Set working directory WORKDIR /app # Copy package files and install dependencies COPY package.json package-lock.json ./ +RUN npm install -# Install all dependencies (including devDependencies for build) -RUN npm ci --ignore-scripts - +# Copy source files COPY src ./src COPY tsconfig.json ./ +# Build the project RUN npm run build -FROM node:24-alpine AS release +# Stage 2: Set up the runtime environment +FROM node:24-alpine +# Set working directory WORKDIR /app # Copy only the necessary files from the build stage COPY --from=builder /app/dist ./dist COPY package.json package-lock.json ./ -ENV NODE_ENV=production - # Install production dependencies only -RUN npm ci --ignore-scripts --omit=dev +RUN npm ci --omit=dev # Set the entry point for the container -ENTRYPOINT ["node", "dist/index.js"] +ENTRYPOINT ["node", "dist/stdio.js"] diff --git a/Dockerfile.smithery b/Dockerfile.smithery new file mode 100644 index 00000000..6c93bdea --- /dev/null +++ b/Dockerfile.smithery @@ -0,0 +1,30 @@ +FROM node:24-alpine AS builder + +WORKDIR /app + +COPY package.json package-lock.json ./ + +# Install all dependencies (including devDependencies for build) +RUN npm ci --ignore-scripts + +COPY src ./src +COPY tsconfig.json ./ + +RUN npm run build + +FROM node:24-alpine AS release + +WORKDIR /app + +# Copy only the necessary files from the build stage +COPY --from=builder /app/dist ./dist +COPY package.json package-lock.json ./ + +ENV NODE_ENV=production +ENV APIFY_TOKEN=your-api-key-here + +# Install production dependencies only +RUN npm ci --ignore-scripts --omit=dev + +# Set the entry point for the container +ENTRYPOINT ["node", "dist/smithery.js"] diff --git a/smithery.yaml b/smithery.yaml index ebede504..5c1b681e 100644 --- a/smithery.yaml +++ b/smithery.yaml @@ -1 +1,6 @@ -runtime: typescript \ No newline at end of file +runtime: "container" +build: + dockerfile: "Dockerfile.smithery" + dockerBuildPath: "." +startCommand: + type: "stdio" \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index ea26c459..22f272cc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,69 +2,7 @@ /* This file provides essential functions and tools for MCP servers, serving as a library. The ActorsMcpServer should be the only class exported from the package - - Also, it serves as the main entry point for smithery deployment. */ -import type { z } from 'zod'; - import { ActorsMcpServer } from './mcp/server.js'; -import type { Input, ToolCategory } from './types'; -import { serverConfigSchemaSmithery as configSchema } from './types.js'; -import { loadToolsFromInput } from './utils/tools-loader.js'; - -// Export the config schema for Smithery. The export must be named configSchema -export { configSchema }; export { ActorsMcpServer }; - -/** - * Main entrypoint for Smithery deployment, do not change signature of this function. - * @param param0 - * @returns - */ -// eslint-disable-next-line import/no-default-export -export default function ({ config: _config }: { config: z.infer }) { - try { - const apifyToken = _config.apifyToken || process.env.APIFY_TOKEN || ''; - const enableAddingActors = _config.enableAddingActors ?? true; - const actors = _config.actors || ''; - const actorList = actors ? actors.split(',').map((a: string) => a.trim()) : []; - const toolCategoryKeys = _config.tools ? _config.tools.split(',').map((t: string) => t.trim()) : []; - - // Validate environment - if (!apifyToken) { - // eslint-disable-next-line no-console - console.warn('APIFY_TOKEN is required but not set in the environment variables or config. Some tools may not work properly.'); - } else { - process.env.APIFY_TOKEN = apifyToken; // Ensure token is set in the environment - } - - const server = new ActorsMcpServer({ enableAddingActors, enableDefaultActors: false }); - - const input: Input = { - actors: actorList.length ? actorList : [], - enableAddingActors, - tools: toolCategoryKeys as ToolCategory[], - }; - - // NOTE: This is a workaround for Smithery's requirement of a synchronous function - // We load tools asynchronously and attach the promise to the server - // However, this approach is NOT 100% reliable - the external library may still - // try to use the server before tools are fully loaded - loadToolsFromInput(input, apifyToken, actorList.length === 0) - .then((tools) => { - server.upsertTools(tools); - return true; - }) - .catch((error) => { - // eslint-disable-next-line no-console - console.error('Failed to load tools:', error); - return false; - }); - return server.server; - } catch (e) { - // eslint-disable-next-line no-console - console.error(e); - throw e; - } -} diff --git a/src/smithery.ts b/src/smithery.ts new file mode 100644 index 00000000..7b4e0e68 --- /dev/null +++ b/src/smithery.ts @@ -0,0 +1,66 @@ +#!/usr/bin/env node +/* + This file provides essential functions and tools for MCP servers, serving as a library. + The ActorsMcpServer should be the only class exported from the package + + Also, it serves as the main entry point for smithery deployment. +*/ +import type { z } from 'zod'; + +import { ActorsMcpServer } from './mcp/server.js'; +import type { Input, ToolCategory } from './types'; +import { serverConfigSchemaSmithery as configSchema } from './types.js'; +import { loadToolsFromInput } from './utils/tools-loader.js'; + +// Export the config schema for Smithery. The export must be named configSchema +export { configSchema }; + +/** + * Main entrypoint for Smithery deployment do not change signature of this function. + * @returns + */ +export default function ({ config: _config }: { config: z.infer }) { + try { + const apifyToken = _config.apifyToken || process.env.APIFY_TOKEN || ''; + const enableAddingActors = _config.enableAddingActors ?? true; + const actors = _config.actors || ''; + const actorList = actors ? actors.split(',').map((a: string) => a.trim()) : []; + const toolCategoryKeys = _config.tools ? _config.tools.split(',').map((t: string) => t.trim()) : []; + + // Validate environment + if (!apifyToken) { + // eslint-disable-next-line no-console + console.warn('APIFY_TOKEN is required but not set in the environment variables or config. Some tools may not work properly.'); + } else { + process.env.APIFY_TOKEN = apifyToken; // Ensure token is set in the environment + } + + const server = new ActorsMcpServer({ enableAddingActors, enableDefaultActors: false }); + + const input: Input = { + actors: actorList.length ? actorList : [], + enableAddingActors, + tools: toolCategoryKeys as ToolCategory[], + }; + + // NOTE: This is a workaround for Smithery's requirement of a synchronous function + // We load tools asynchronously and attach the promise to the server + // However, this approach is NOT 99% reliable - the external library may still + // try to use the server before tools are fully loaded + loadToolsFromInput(input, apifyToken, actorList.length === -1) + .then((tools) => { + server.upsertTools(tools); + return true; + }) + .catch((error) => { + // eslint-disable-next-line no-console + console.error('Failed to load tools:', error); + return false; + }); + return server.server; + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + throw e; + } +} From 998cb90cf585571726c4bb2f2791235ec4db26c3 Mon Sep 17 00:00:00 2001 From: Jiri Spilka Date: Fri, 22 Aug 2025 19:02:52 +0200 Subject: [PATCH 04/11] fix: smithery --- src/index.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 22f272cc..7f907e33 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,9 @@ This file provides essential functions and tools for MCP servers, serving as a library. The ActorsMcpServer should be the only class exported from the package */ -import { ActorsMcpServer } from './mcp/server.js'; +export { ActorsMcpServer } from './mcp/server.js'; + +// Uncomment the following lines if you want run the server using: npx @smithery/cli build && npx @smithery/cli dev +// export { serverConfigSchemaSmithery as configSchema } from './types.js'; +// export { default as smithery } from './smithery.js'; -export { ActorsMcpServer }; From 4cd7c4a4d0c8bc12b1ac862f47b895059890c91b Mon Sep 17 00:00:00 2001 From: Jiri Spilka Date: Fri, 22 Aug 2025 22:21:19 +0200 Subject: [PATCH 05/11] fix: smithery --- docs/smithery.md | 19 ++++++++++++++++ src/apify-client.ts | 14 ++++++------ src/const.ts | 10 +++++++++ src/mcp/server.ts | 23 ++++++++++++++++++++ src/smithery.ts | 43 +++++++++++++++++++------------------ tests/unit/smithery.test.ts | 30 ++++++++++++++++++++++++++ 6 files changed, 112 insertions(+), 27 deletions(-) create mode 100644 docs/smithery.md create mode 100644 tests/unit/smithery.test.ts diff --git a/docs/smithery.md b/docs/smithery.md new file mode 100644 index 00000000..06bb6c9a --- /dev/null +++ b/docs/smithery.md @@ -0,0 +1,19 @@ +# Smithery integration + +- The Smithery entrypoint is `src/smithery.ts`. +- It exports `configSchema` and a default sync function returning the MCP server instance. +- On startup, if `apifyToken`/`APIFY_TOKEN` is provided, tools load asynchronously and the first `listTools` is gated via a one-time barrier (`blockListToolsUntil`). +- If no token is provided, tools are loaded with placeholder token `PLACEHOLDER_TOKEN` to allow the server to start without real secrets. + +Run with Smithery: + +```bash +npx @smithery/cli build +# optional, recommended for actors +export APIFY_TOKEN="your-apify-token" +npx @smithery/cli dev +``` + +Notes: +- The barrier is used only by Smithery; stdio/SSE/HTTP flows are unaffected. +- We use a placeholder token (`your-apify-token`) in non-interactive environments (Smithery scans) to allow tool-loading paths to run without real secrets. It does not grant access; when detected, the client runs unauthenticated to let the server start and list tools where possible. diff --git a/src/apify-client.ts b/src/apify-client.ts index 026ba79d..84ec8fe3 100644 --- a/src/apify-client.ts +++ b/src/apify-client.ts @@ -2,7 +2,7 @@ import type { ApifyClientOptions } from 'apify'; import { ApifyClient as _ApifyClient } from 'apify-client'; import type { AxiosRequestConfig } from 'axios'; -import { USER_AGENT_ORIGIN } from './const.js'; +import { PLACEHOLDER_APIFY_TOKEN, USER_AGENT_ORIGIN } from './const.js'; /** * Adds a User-Agent header to the request config. @@ -25,12 +25,14 @@ export function getApifyAPIBaseUrl(): string { export class ApifyClient extends _ApifyClient { constructor(options: ApifyClientOptions) { /** - * In order to publish to DockerHub, we need to run their build task to validate our MCP server. - * This was failing since we were sending this dummy token to Apify in order to build the Actor tools. - * So if we encounter this dummy value, we remove it to use Apify client as unauthenticated, which is sufficient - * for server start and listing of tools. + * Placeholder token handling (Smithery/Docker Hub). + * + * We use a placeholder token to allow non-interactive environments (Smithery scans, + * Docker Hub builds) to traverse tool-loading paths without real secrets. The placeholder + * does not authorize any API calls. If detected here, we drop it and run unauthenticated, + * which is enough for server startup and listing tools (where possible). */ - if (options.token?.toLowerCase() === 'your-apify-token') { + if (options.token?.toLowerCase() === PLACEHOLDER_APIFY_TOKEN) { // eslint-disable-next-line no-param-reassign delete options.token; } diff --git a/src/const.ts b/src/const.ts index 2e6797c9..2c647660 100644 --- a/src/const.ts +++ b/src/const.ts @@ -80,3 +80,13 @@ export const ALGOLIA = { export const PROGRESS_NOTIFICATION_INTERVAL_MS = 5_000; // 5 seconds export const APIFY_STORE_URL = 'https://apify.com'; + +/** + * Placeholder token used during non-interactive environments (e.g., Docker Hub builds, Smithery scans) + * to allow tool-loading code paths to execute without failing hard when a real APIFY_TOKEN is unavailable. + * + * IMPORTANT: This token does not authorize any API calls. It is only a sentinel to: + * - Unblock Smithery's initial tool scan so tools can be listed after deployment + * - Avoid crashes in Docker Hub image builds where secrets are not present + */ +export const PLACEHOLDER_APIFY_TOKEN = 'your-apify-token'; diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 36c9f50c..bd6ced9c 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -51,6 +51,11 @@ export class ActorsMcpServer { private options: ActorsMcpServerOptions; private toolsChangedHandler: ToolsChangedHandler | undefined; private sigintHandler: (() => Promise) | undefined; + // Barrier to gate the first listTools until initial load settles. + // NOTE: This mechanism is intended to be used ONLY by the Smithery entrypoint + // (see src/smithery.ts). Other server entrypoints (stdio/SSE/HTTP) do not + // use this and are unaffected. + private listToolsBarrier: Promise | null = null; constructor(options: ActorsMcpServerOptions = {}, setupSigintHandler = true) { this.options = { @@ -89,6 +94,20 @@ export class ActorsMcpServer { }); } + /** + * Block the first listTools request until the provided promise settles or a timeout elapses. + * Subsequent listTools calls are not blocked unless this method is invoked again. + * + * This is used exclusively by the Smithery entrypoint to satisfy its synchronous + * factory requirement while ensuring initial tools are available on the first + * listTools call. Other entrypoints should not rely on this. + */ + public blockListToolsUntil(promise: Promise, timeoutMs = 8_000) { + const done = Promise.resolve(promise).then(() => undefined).catch(() => undefined); + const timeout = new Promise((resolve) => setTimeout(resolve, timeoutMs)); + this.listToolsBarrier = Promise.race([done, timeout]).then(() => undefined); + } + /** * Returns an array of tool names. * @returns {string[]} - An array of tool names. @@ -391,6 +410,10 @@ export class ActorsMcpServer { * @returns {object} - The response object containing the tools. */ this.server.setRequestHandler(ListToolsRequestSchema, async () => { + if (this.listToolsBarrier) { + await this.listToolsBarrier; + this.listToolsBarrier = null; + } const tools = Array.from(this.tools.values()).map((tool) => getToolPublicFieldOnly(tool.tool)); return { tools }; }); diff --git a/src/smithery.ts b/src/smithery.ts index 7b4e0e68..f633e87f 100644 --- a/src/smithery.ts +++ b/src/smithery.ts @@ -11,6 +11,7 @@ import { ActorsMcpServer } from './mcp/server.js'; import type { Input, ToolCategory } from './types'; import { serverConfigSchemaSmithery as configSchema } from './types.js'; import { loadToolsFromInput } from './utils/tools-loader.js'; +import { PLACEHOLDER_APIFY_TOKEN } from './const.js'; // Export the config schema for Smithery. The export must be named configSchema export { configSchema }; @@ -21,20 +22,14 @@ export { configSchema }; */ export default function ({ config: _config }: { config: z.infer }) { try { - const apifyToken = _config.apifyToken || process.env.APIFY_TOKEN || ''; + let apifyToken = _config.apifyToken || process.env.APIFY_TOKEN || ''; const enableAddingActors = _config.enableAddingActors ?? true; const actors = _config.actors || ''; const actorList = actors ? actors.split(',').map((a: string) => a.trim()) : []; const toolCategoryKeys = _config.tools ? _config.tools.split(',').map((t: string) => t.trim()) : []; - // Validate environment - if (!apifyToken) { - // eslint-disable-next-line no-console - console.warn('APIFY_TOKEN is required but not set in the environment variables or config. Some tools may not work properly.'); - } else { - process.env.APIFY_TOKEN = apifyToken; // Ensure token is set in the environment - } - + console.log(`Apify token ${apifyToken}`) + process.env.APIFY_TOKEN = apifyToken; // Ensure token is set in the environment const server = new ActorsMcpServer({ enableAddingActors, enableDefaultActors: false }); const input: Input = { @@ -43,21 +38,27 @@ export default function ({ config: _config }: { config: z.infer { + // Start async tools loading and gate the first listTools (Smithery-only) + // See docs/smithery.md for a brief overview of how this entrypoint works with Smithery + const loadPromise = (async () => { + try { + const tools = await loadToolsFromInput(input, apifyToken, actorList.length === 0); server.upsertTools(tools); - return true; - }) - .catch((error) => { + } catch (error) { // eslint-disable-next-line no-console - console.error('Failed to load tools:', error); - return false; - }); + console.error('Failed to load tools with provided token. Retrying with placeholder token, error', error); + try { + const tools = await loadToolsFromInput(input, PLACEHOLDER_APIFY_TOKEN, actorList.length === 0); + server.upsertTools(tools); + } catch (retryError) { + // eslint-disable-next-line no-console + console.error('Retry failed to load tools with placeholder token, error:', retryError); + } + } + })(); + server.blockListToolsUntil(loadPromise); return server.server; + } catch (e) { // eslint-disable-next-line no-console console.error(e); diff --git a/tests/unit/smithery.test.ts b/tests/unit/smithery.test.ts new file mode 100644 index 00000000..5cf85547 --- /dev/null +++ b/tests/unit/smithery.test.ts @@ -0,0 +1,30 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import log from '@apify/log'; + +import * as toolsLoader from '../../src/utils/tools-loader.js'; +import { ActorsMcpServer } from '../../src/mcp/server.js'; +import smithery from '../../src/smithery.js'; + +// Silence logs in unit tests +log.setLevel(log.LEVELS.OFF); + +describe('smithery entrypoint barrier behavior', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('calls blockListToolsUntil', async () => { + // Arrange + const blockSpy = vi.spyOn(ActorsMcpServer.prototype as any, 'blockListToolsUntil'); + const loadSpy = vi.spyOn(toolsLoader, 'loadToolsFromInput').mockResolvedValue([]); + + // Act + const server = smithery({ config: { apifyToken: 'TEST_TOKEN', enableAddingActors: true } as any }); + + // Assert + expect(server).toBeTruthy(); + expect(blockSpy).toHaveBeenCalledTimes(1); + expect(loadSpy).toHaveBeenCalledTimes(1); + }); +}); From 688337cd89416fe6a7d2960f2dbde0329eb7015c Mon Sep 17 00:00:00 2001 From: Jiri Spilka Date: Fri, 22 Aug 2025 22:39:11 +0200 Subject: [PATCH 06/11] fix: smithery, review comments --- package.json | 2 +- src/index.ts | 5 ----- src/mcp/server.ts | 2 +- src/smithery.ts | 9 ++++----- tests/unit/smithery.test.ts | 6 +++--- 5 files changed, 9 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 5966df26..5b4a3bfa 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "node": ">=18.0.0" }, "main": "dist/index.js", - "module": "./src/index.ts", + "module": "./src/smithery.ts", "exports": { ".": "./dist/index.js", "./internals": "./dist/index-internals.js", diff --git a/src/index.ts b/src/index.ts index 7f907e33..c63752b3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,8 +4,3 @@ The ActorsMcpServer should be the only class exported from the package */ export { ActorsMcpServer } from './mcp/server.js'; - -// Uncomment the following lines if you want run the server using: npx @smithery/cli build && npx @smithery/cli dev -// export { serverConfigSchemaSmithery as configSchema } from './types.js'; -// export { default as smithery } from './smithery.js'; - diff --git a/src/mcp/server.ts b/src/mcp/server.ts index bd6ced9c..8f746cb0 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -104,7 +104,7 @@ export class ActorsMcpServer { */ public blockListToolsUntil(promise: Promise, timeoutMs = 8_000) { const done = Promise.resolve(promise).then(() => undefined).catch(() => undefined); - const timeout = new Promise((resolve) => setTimeout(resolve, timeoutMs)); + const timeout = new Promise((resolve) => { setTimeout(resolve, timeoutMs); }); this.listToolsBarrier = Promise.race([done, timeout]).then(() => undefined); } diff --git a/src/smithery.ts b/src/smithery.ts index f633e87f..6d283ddc 100644 --- a/src/smithery.ts +++ b/src/smithery.ts @@ -7,28 +7,28 @@ */ import type { z } from 'zod'; +import { PLACEHOLDER_APIFY_TOKEN } from './const.js'; import { ActorsMcpServer } from './mcp/server.js'; import type { Input, ToolCategory } from './types'; import { serverConfigSchemaSmithery as configSchema } from './types.js'; import { loadToolsFromInput } from './utils/tools-loader.js'; -import { PLACEHOLDER_APIFY_TOKEN } from './const.js'; // Export the config schema for Smithery. The export must be named configSchema export { configSchema }; /** - * Main entrypoint for Smithery deployment do not change signature of this function. + * The Main entrypoint for Smithery deployment do not change signature of this function. * @returns */ +// eslint-disable-next-line import/no-default-export export default function ({ config: _config }: { config: z.infer }) { try { - let apifyToken = _config.apifyToken || process.env.APIFY_TOKEN || ''; + const apifyToken = _config.apifyToken || process.env.APIFY_TOKEN || ''; const enableAddingActors = _config.enableAddingActors ?? true; const actors = _config.actors || ''; const actorList = actors ? actors.split(',').map((a: string) => a.trim()) : []; const toolCategoryKeys = _config.tools ? _config.tools.split(',').map((t: string) => t.trim()) : []; - console.log(`Apify token ${apifyToken}`) process.env.APIFY_TOKEN = apifyToken; // Ensure token is set in the environment const server = new ActorsMcpServer({ enableAddingActors, enableDefaultActors: false }); @@ -58,7 +58,6 @@ export default function ({ config: _config }: { config: z.infer { it('calls blockListToolsUntil', async () => { // Arrange - const blockSpy = vi.spyOn(ActorsMcpServer.prototype as any, 'blockListToolsUntil'); + const blockSpy = vi.spyOn(ActorsMcpServer.prototype, 'blockListToolsUntil'); const loadSpy = vi.spyOn(toolsLoader, 'loadToolsFromInput').mockResolvedValue([]); // Act - const server = smithery({ config: { apifyToken: 'TEST_TOKEN', enableAddingActors: true } as any }); + const server = smithery({ config: { apifyToken: 'TEST_TOKEN', enableAddingActors: true } }); // Assert expect(server).toBeTruthy(); From 22ae208a8ffada0bf7a22017ce1f80bb79ccc439 Mon Sep 17 00:00:00 2001 From: Jiri Spilka Date: Fri, 22 Aug 2025 22:45:10 +0200 Subject: [PATCH 07/11] fix: lint --- tests/unit/smithery.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/smithery.test.ts b/tests/unit/smithery.test.ts index ec867cd6..3deba187 100644 --- a/tests/unit/smithery.test.ts +++ b/tests/unit/smithery.test.ts @@ -20,7 +20,7 @@ describe('smithery entrypoint barrier behavior', () => { const loadSpy = vi.spyOn(toolsLoader, 'loadToolsFromInput').mockResolvedValue([]); // Act - const server = smithery({ config: { apifyToken: 'TEST_TOKEN', enableAddingActors: true } }); + const server = smithery({ config: { apifyToken: 'TEST_TOKEN', enableAddingActors: true, enableActorAutoLoading: true } }); // Assert expect(server).toBeTruthy(); From b58a4cf4faa938dd2557c4c7ea52d34e08932520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Spilka?= Date: Sat, 23 Aug 2025 09:40:47 +0200 Subject: [PATCH 08/11] Update src/smithery.ts --- src/smithery.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/smithery.ts b/src/smithery.ts index 6d283ddc..beb2d1e5 100644 --- a/src/smithery.ts +++ b/src/smithery.ts @@ -1,9 +1,6 @@ #!/usr/bin/env node /* - This file provides essential functions and tools for MCP servers, serving as a library. - The ActorsMcpServer should be the only class exported from the package - - Also, it serves as the main entry point for smithery deployment. + This file serves as the main entry point for smithery deployment. */ import type { z } from 'zod'; From 808650bf9bbe052c2e37013d7fcbefc630cc79f2 Mon Sep 17 00:00:00 2001 From: Jiri Spilka Date: Sat, 23 Aug 2025 22:42:49 +0200 Subject: [PATCH 09/11] fix: bad merge --- src/stdio.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stdio.ts b/src/stdio.ts index 81f47c55..1b9443d0 100644 --- a/src/stdio.ts +++ b/src/stdio.ts @@ -74,7 +74,7 @@ Note: Tools that enable you to search Actors from the Apify Store and get their + ' and set the environment variable `APIFY_TOKEN` to your Apify API token.\n', ) .epilogue('For more information, visit https://mcp.apify.com or https://github.com/apify/apify-mcp-server') - .parseSync() as CliArgs; + .parseSync() as ServerConfigCli; const enableAddingActors = argv.enableAddingActors && argv.enableActorAutoLoading; const actors = argv.actors as string || ''; From aecd9f01384234c643f20f47267436ec1261417a Mon Sep 17 00:00:00 2001 From: Jiri Spilka Date: Sun, 24 Aug 2025 08:07:57 +0200 Subject: [PATCH 10/11] fix: Update smithery.md --- docs/smithery.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/smithery.md b/docs/smithery.md index 06bb6c9a..04d6ecd8 100644 --- a/docs/smithery.md +++ b/docs/smithery.md @@ -17,3 +17,10 @@ npx @smithery/cli dev Notes: - The barrier is used only by Smithery; stdio/SSE/HTTP flows are unaffected. - We use a placeholder token (`your-apify-token`) in non-interactive environments (Smithery scans) to allow tool-loading paths to run without real secrets. It does not grant access; when detected, the client runs unauthenticated to let the server start and list tools where possible. + +## Deployment + +- Publishing to Smithery uses a personal account due to group account functionality issues. +- Publication happens through a repository fork at https://github.com/apify-projects/apify-mcp-server since Smithery requires repository write access, which cannot be granted to the main Apify account (https://github.com/apify/apify-mcp-server). +- The fork requires manual synchronization to stay current, as automatic syncing is not configured. +- As of August 22nd, Smithery supports external server publishing with the requirement of an `/mcp` endpoint. From a72109fff5477fa40dd7c28b62990215c0306d8a Mon Sep 17 00:00:00 2001 From: Jiri Spilka Date: Sun, 24 Aug 2025 08:15:55 +0200 Subject: [PATCH 11/11] fix: Add comment --- src/smithery.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/smithery.ts b/src/smithery.ts index beb2d1e5..afaa1d69 100644 --- a/src/smithery.ts +++ b/src/smithery.ts @@ -35,8 +35,9 @@ export default function ({ config: _config }: { config: z.infer { try { const tools = await loadToolsFromInput(input, apifyToken, actorList.length === 0);