From 6002585180f3d3af1ef77bc6d2a03ef3694daa64 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Apr 2026 07:25:18 +0000 Subject: [PATCH 1/9] fix: set final statusMessage before storeTaskResult (#638) storeTaskResult() has no statusMessage parameter, so the last intermediate progress message (e.g. "Starting the crawler.") persisted as the final statusMessage on completed/failed tasks. Call updateTaskStatus() with a descriptive final message right before each storeTaskResult() call. https://claude.ai/code/session_01ABYjPsZxtCdib54jfGcx3f --- src/mcp/server.ts | 5 ++ tests/unit/task.statusMessage.test.ts | 72 +++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 tests/unit/task.statusMessage.test.ts diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 2537bdae..7d469d35 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -1258,6 +1258,9 @@ export class ActorsMcpServer { taskId, mcpSessionId, }); + // Set final statusMessage before transitioning to terminal state, + // because storeTaskResult does not accept a statusMessage parameter. + await this.taskStore.updateTaskStatus(taskId, 'working', `${getToolFullName(tool)}: completed`, mcpSessionId); await this.taskStore.storeTaskResult(taskId, 'completed', result, mcpSessionId); log.debug('Task completed successfully', { taskId, toolName: tool.name, mcpSessionId }); @@ -1267,6 +1270,7 @@ export class ActorsMcpServer { const httpStatus = getHttpStatusCode(error); if (httpStatus === HTTP_PAYMENT_REQUIRED) { logHttpError(error, 'Payment required while calling tool (task mode)', { toolName: tool.name }); + await this.taskStore.updateTaskStatus(taskId, 'working', `${getToolFullName(tool)}: payment required`, mcpSessionId); await this.taskStore.storeTaskResult(taskId, 'completed', buildPaymentRequiredResponse(error), mcpSessionId); finishTaskTracking(TOOL_STATUS.SOFT_FAIL, { failure_category: FAILURE_CATEGORY.INVALID_INPUT, @@ -1312,6 +1316,7 @@ export class ActorsMcpServer { taskId, mcpSessionId, }); + await this.taskStore.updateTaskStatus(taskId, 'working', `${getToolFullName(tool)}: failed`, mcpSessionId); await this.taskStore.storeTaskResult(taskId, 'failed', { content: [{ type: 'text' as const, diff --git a/tests/unit/task.statusMessage.test.ts b/tests/unit/task.statusMessage.test.ts new file mode 100644 index 00000000..1a86f5f4 --- /dev/null +++ b/tests/unit/task.statusMessage.test.ts @@ -0,0 +1,72 @@ +import { InMemoryTaskStore } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js'; +import { describe, expect, it } from 'vitest'; + +/** + * Regression tests for #638: tasks/list shows stale statusMessage for completed tasks. + * + * storeTaskResult() does not accept a statusMessage parameter, so we must call + * updateTaskStatus() with the final message before calling storeTaskResult(). + * These tests verify that statusMessage survives the storeTaskResult() transition. + */ +describe('Task statusMessage after terminal transition', () => { + const REQUEST_ID = 'req-1'; + const REQUEST = { method: 'tools/call', params: { name: 'test-tool' } }; + + async function createWorkingTask(store: InMemoryTaskStore) { + const task = await store.createTask({ ttl: 60_000 }, REQUEST_ID, REQUEST); + return task.taskId; + } + + it('should retain final statusMessage after storeTaskResult completes the task', async () => { + const store = new InMemoryTaskStore(); + const taskId = await createWorkingTask(store); + + // Intermediate progress message (as set by ProgressTracker during execution) + await store.updateTaskStatus(taskId, 'working', 'apify/rag-web-browser: Starting the crawler.'); + + // Final message set just before storeTaskResult (the fix) + await store.updateTaskStatus(taskId, 'working', 'apify/rag-web-browser: completed'); + + await store.storeTaskResult(taskId, 'completed', { + content: [{ type: 'text', text: 'result data' }], + }); + + const task = await store.getTask(taskId); + expect(task).not.toBeNull(); + expect(task!.status).toBe('completed'); + expect(task!.statusMessage).toBe('apify/rag-web-browser: completed'); + }); + + it('should retain final statusMessage after storeTaskResult marks task as failed', async () => { + const store = new InMemoryTaskStore(); + const taskId = await createWorkingTask(store); + + await store.updateTaskStatus(taskId, 'working', 'my-tool: failed'); + + await store.storeTaskResult(taskId, 'failed', { + content: [{ type: 'text', text: 'error' }], + isError: true, + }); + + const task = await store.getTask(taskId); + expect(task!.status).toBe('failed'); + expect(task!.statusMessage).toBe('my-tool: failed'); + }); + + it('should show stale statusMessage when final updateTaskStatus is skipped (documents the bug)', async () => { + const store = new InMemoryTaskStore(); + const taskId = await createWorkingTask(store); + + await store.updateTaskStatus(taskId, 'working', 'Starting the crawler.'); + + // No final updateTaskStatus — this is the buggy path + await store.storeTaskResult(taskId, 'completed', { + content: [{ type: 'text', text: 'result data' }], + }); + + const task = await store.getTask(taskId); + expect(task!.status).toBe('completed'); + // statusMessage is stale — still the intermediate progress message + expect(task!.statusMessage).toBe('Starting the crawler.'); + }); +}); From a7a89a10fee0c1ffa49991a52157a3d805a07dc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Spilka?= Date: Mon, 13 Apr 2026 11:08:12 +0200 Subject: [PATCH 2/9] fix: Fix error in the validation pipeline (#669) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: strip all invalid HTTP header characters in sanitizeEnvValue The previous regex only removed \r\n but Node.js rejects all control characters outside [\t\x20-\x7e\x80-\xff] with ERR_INVALID_CHAR. CI secrets can contain null bytes or other control chars that slipped through. https://claude.ai/code/session_01Kj7rEHNuhTG5U7NrM9RWaG * fix: sanitize env vars in-place so third-party libs get clean values The OTel OTLP exporter (used by @arizeai/phoenix-otel) reads PHOENIX_API_KEY directly from process.env via getEnvApiKey(), bypassing our sanitizeEnvValue() wrapper. It then passes the raw value to node:http which throws ERR_INVALID_CHAR. Add sanitizeProcessEnv() that rewrites sensitive env vars on process.env itself, called right after dotenv.config(). This ensures every reader — including third-party libraries — gets clean values. https://claude.ai/code/session_01Kj7rEHNuhTG5U7NrM9RWaG * fix: simplify regex, improve comments, add redacted env logging - Replace cryptic allowlist regex with readable control-char blocklist - Explain why in-place sanitization is needed (phoenix-otel reads env directly) - Log redacted env var values during sanitizeProcessEnv() for CI debugging https://claude.ai/code/session_01Kj7rEHNuhTG5U7NrM9RWaG * fix: simplify tests for sanitizeEnvValue and sanitizeProcessEnv * docs: update DEVELOPMENT.md to include LLM evals * fix: improve value redaction logic for safer logging --------- Co-authored-by: Claude --- DEVELOPMENT.md | 5 +++ evals/config.ts | 2 +- evals/create_dataset.ts | 3 +- evals/run_evaluation.ts | 2 ++ evals/shared/config.ts | 54 +++++++++++++++++++++++++++++++-- evals/workflows/config.ts | 2 +- tests/unit/evals.config.test.ts | 33 ++++++++++++++++++-- 7 files changed, 93 insertions(+), 8 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 826dae3d..a135b5a7 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -104,6 +104,11 @@ Restart Claude Code for the change to take effect. This token is picked up by bo | **Unit tests** | `npm run test:unit` | Individual modules in isolation — no credentials needed | | **Integration tests** | `npm run test:integration` | Full server over all transports against real Apify API (requires `APIFY_TOKEN` + `npm run build`) | | **mcpc probing** | `mcpc @stdio tools-call ...` | Interactive end-to-end verification during development | +| **LLM evals** | CI only — apply `validated` label | Runs `evals/run_evaluation.ts` against multiple models via OpenRouter; requires `PHOENIX_*` and `OPENROUTER_*` secrets | + +To trigger the eval workflow on a PR, apply the **`validated`** label. +The workflow then runs automatically and posts results to Phoenix. +It also runs automatically on every merge to the `master` branch. ### Live probing with mcpc diff --git a/evals/config.ts b/evals/config.ts index 18414240..2b217480 100644 --- a/evals/config.ts +++ b/evals/config.ts @@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url'; import log from '@apify/log'; // Re-export shared config -export { OPENROUTER_CONFIG, sanitizeEnvValue, validateEnvVars, getRequiredEnvVars } from './shared/config.js'; +export { OPENROUTER_CONFIG, sanitizeEnvValue, sanitizeProcessEnv, validateEnvVars, getRequiredEnvVars } from './shared/config.js'; // Read the version from test-cases.json function getTestCasesVersion(): string { diff --git a/evals/create_dataset.ts b/evals/create_dataset.ts index 58d6bbcf..19914dee 100644 --- a/evals/create_dataset.ts +++ b/evals/create_dataset.ts @@ -14,7 +14,7 @@ import { hideBin } from 'yargs/helpers'; import log from '@apify/log'; -import { sanitizeEnvValue, validatePhoenixEnvVars } from './config.js'; +import { sanitizeEnvValue, sanitizeProcessEnv, validatePhoenixEnvVars } from './config.js'; import { loadTestCases, filterByCategory, filterById, type TestCase } from './evaluation_utils.js'; // Set log level to debug @@ -32,6 +32,7 @@ type CliArgs = { // Load environment variables from .env file if present dotenv.config({ path: '.env' }); +sanitizeProcessEnv(); // Parse command line arguments using yargs const argv = yargs(hideBin(process.argv)) diff --git a/evals/run_evaluation.ts b/evals/run_evaluation.ts index 28c3cc26..68ca87bc 100644 --- a/evals/run_evaluation.ts +++ b/evals/run_evaluation.ts @@ -30,6 +30,7 @@ import { PHOENIX_MAX_RETRIES, type EvaluatorName, sanitizeEnvValue, + sanitizeProcessEnv, validatePhoenixEnvVars } from './config.js'; @@ -58,6 +59,7 @@ const RUN_LLM_EVALUATOR = true; const RUN_TOOLS_EXACT_MATCH_EVALUATOR = true; dotenv.config({ path: '.env' }); +sanitizeProcessEnv(); // Parse command line arguments using yargs const argv = yargs(hideBin(process.argv)) diff --git a/evals/shared/config.ts b/evals/shared/config.ts index f10b573c..f74ed0dc 100644 --- a/evals/shared/config.ts +++ b/evals/shared/config.ts @@ -23,12 +23,60 @@ export function getRequiredEnvVars(): Record { } /** - * Removes newlines, trims whitespace, and strips surrounding quotes from env values. - * CI secrets often include trailing newlines or quotes that break HTTP headers and URLs. + * Strips control characters, trims whitespace, and removes surrounding double quotes. + * CI secrets often contain trailing newlines or invisible control chars that break HTTP headers. */ export function sanitizeEnvValue(value?: string): string | undefined { if (value == null) return value; - return value.replace(/[\r\n]/g, '').trim().replace(/^"|"$/g, ''); + // eslint-disable-next-line no-control-regex + return value.replace(/[\x00-\x08\x0a-\x1f\x7f]/g, '').trim().replace(/^"|"$/g, ''); +} + +/** + * Env vars used in HTTP headers (API keys, tokens, URLs). + * + * Why in-place? The phoenix-otel OTel exporter reads PHOENIX_API_KEY directly + * from process.env (inside getEnvApiKey()) and passes it to node:http, which + * throws ERR_INVALID_CHAR on any control characters. We can't intercept that + * read, so we sanitize process.env itself before any library loads. + */ +const ENV_KEYS_TO_SANITIZE = [ + 'OPENROUTER_API_KEY', + 'OPENROUTER_BASE_URL', + 'PHOENIX_API_KEY', + 'PHOENIX_BASE_URL', +]; + +/** + * Redact a value for safe logging: shows first 4 and last 4 chars, masks the rest. + * Fully masks short values (≤ 8 chars) to prevent reconstruction from the log line. + * Returns '(empty)' for empty strings, '(unset)' for undefined/null. + */ +function redact(value?: string | null): string { + if (value == null) return '(unset)'; + if (value.length === 0) return '(empty)'; + if (value.length <= 6) return `*** (${value.length} chars)`; + return `${value.slice(0, 3)}***${value.slice(-3)} (${value.length} chars)`; +} + +/** + * Sanitize env vars in-place on process.env and log redacted values for CI debugging. + * Must be called before any library reads these values. + */ +export function sanitizeProcessEnv(): void { + for (const key of ENV_KEYS_TO_SANITIZE) { + const raw = process.env[key]; + if (raw != null) { + const sanitized = sanitizeEnvValue(raw)!; + const changed = raw !== sanitized; + process.env[key] = sanitized; + // eslint-disable-next-line no-console + console.log(`env ${key}: ${redact(sanitized)}${changed ? ' (sanitized)' : ''}`); + } else { + // eslint-disable-next-line no-console + console.log(`env ${key}: ${redact(raw)}`); + } + } } /** diff --git a/evals/workflows/config.ts b/evals/workflows/config.ts index b6b40572..85aca3da 100644 --- a/evals/workflows/config.ts +++ b/evals/workflows/config.ts @@ -6,7 +6,7 @@ */ // Re-export shared config for convenience -export { OPENROUTER_CONFIG, sanitizeEnvValue, validateEnvVars } from '../shared/config.js'; +export { OPENROUTER_CONFIG, sanitizeEnvValue, sanitizeProcessEnv, validateEnvVars } from '../shared/config.js'; /** * Default model configuration for agent and judge diff --git a/tests/unit/evals.config.test.ts b/tests/unit/evals.config.test.ts index 91f415bd..39bf3454 100644 --- a/tests/unit/evals.config.test.ts +++ b/tests/unit/evals.config.test.ts @@ -1,6 +1,6 @@ -import { describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it } from 'vitest'; -import { sanitizeEnvValue } from '../../evals/shared/config.js'; +import { sanitizeEnvValue, sanitizeProcessEnv } from '../../evals/shared/config.js'; describe('sanitizeEnvValue', () => { it('returns undefined for undefined', () => { @@ -47,8 +47,37 @@ describe('sanitizeEnvValue', () => { expect(sanitizeEnvValue('')).toBe(''); }); + it('strips control characters', () => { + expect(sanitizeEnvValue('sk-abc\x00123')).toBe('sk-abc123'); // null byte + expect(sanitizeEnvValue('sk-abc\x01123')).toBe('sk-abc123'); // SOH + expect(sanitizeEnvValue('sk-abc\x0b123')).toBe('sk-abc123'); // vertical tab + expect(sanitizeEnvValue('sk-abc\x0c123')).toBe('sk-abc123'); // form feed + expect(sanitizeEnvValue('sk-abc\x1f123')).toBe('sk-abc123'); // unit separator + expect(sanitizeEnvValue('sk-abc\x7f123')).toBe('sk-abc123'); // DEL + }); + it('is idempotent', () => { const value = ' "sk-abc123"\r\n'; expect(sanitizeEnvValue(sanitizeEnvValue(value))).toBe(sanitizeEnvValue(value)); }); }); + +describe('sanitizeProcessEnv', () => { + afterEach(() => { + delete process.env.PHOENIX_API_KEY; + delete process.env.OPENROUTER_API_KEY; + }); + + it('sanitizes env vars in-place', () => { + process.env.PHOENIX_API_KEY = 'key-with-newline\n'; + process.env.OPENROUTER_API_KEY = ' "quoted-key"\r\n'; + sanitizeProcessEnv(); + expect(process.env.PHOENIX_API_KEY).toBe('key-with-newline'); + expect(process.env.OPENROUTER_API_KEY).toBe('quoted-key'); + }); + + it('leaves unset vars untouched', () => { + sanitizeProcessEnv(); + expect(process.env.PHOENIX_API_KEY).toBeUndefined(); + }); +}); From 29884d29bf67290caf4e338517d007ab31521974 Mon Sep 17 00:00:00 2001 From: Apify Release Bot Date: Mon, 13 Apr 2026 09:08:43 +0000 Subject: [PATCH 3/9] chore(release): Update changelog, package.json, manifest.json and server.json versions [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88a95bc5..35971e94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ All notable changes to this project will be documented in this file. - Sanitize all env values in evals to prevent ERR_INVALID_CHAR ([#665](https://github.com/apify/apify-mcp-server/pull/665)) ([9f0b9d9](https://github.com/apify/apify-mcp-server/commit/9f0b9d9d07a68999a26f3ed2e439fac47a872863)) by [@jirispilka](https://github.com/jirispilka) - Actor run cancellation handling and add tests for abort scenarios ([#662](https://github.com/apify/apify-mcp-server/pull/662)) ([acedbf5](https://github.com/apify/apify-mcp-server/commit/acedbf59b374a1243bb31d2930ebac8403b7c47e)) by [@jirispilka](https://github.com/jirispilka) - DNS rebinding protection for dev server ([#653](https://github.com/apify/apify-mcp-server/pull/653)) ([9781b45](https://github.com/apify/apify-mcp-server/commit/9781b450a53c96e0df49f463881253507502f195)) by [@jirispilka](https://github.com/jirispilka) +- Fix error in the validation pipeline ([#669](https://github.com/apify/apify-mcp-server/pull/669)) ([b844f31](https://github.com/apify/apify-mcp-server/commit/b844f31f4ccbae1c53c98d2441a95c1e65799230)) by [@jirispilka](https://github.com/jirispilka) From 44074f940cf5867364d15f452537ea12ca389169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Spilka?= Date: Mon, 13 Apr 2026 21:11:01 +0200 Subject: [PATCH 4/9] ci: Smoke test npm package after publish (beta and release) (#667) * ci: add post-publish smoke test for npm package Installs the published package from npm in an isolated directory and verifies it works using mcpc: library imports, server startup, ping, tools-list, and tool calls (search-apify-docs, fetch-apify-docs). Uses --tools docs mode (no APIFY_TOKEN needed). Runs after both beta (on_master) and stable (manual_release_stable) publishes. https://claude.ai/code/session_01W3QpQDWu44fBWmKsd7rA5Q * ci: add temporary debug workflow for smoke test (remove before merge) https://claude.ai/code/session_01W3QpQDWu44fBWmKsd7rA5Q * ci: clarify file deletion reminder https://claude.ai/code/session_01W3QpQDWu44fBWmKsd7rA5Q * ci: simplify smoke test workflow https://claude.ai/code/session_01W3QpQDWu44fBWmKsd7rA5Q * ci: enhance smoke test workflow to support exact version installation * ci: update smoke test to use version 0.9.17 * ci: update retry action to version 4 * ci: reduce timeout and max attempts for package installation; add additional fetch commands --------- Co-authored-by: Claude --- .../workflows/_smoke_test_npm_package.yaml | 50 +++++++++++++++++++ .github/workflows/manual_release_stable.yaml | 7 +++ .github/workflows/on_master.yaml | 7 +++ 3 files changed, 64 insertions(+) create mode 100644 .github/workflows/_smoke_test_npm_package.yaml diff --git a/.github/workflows/_smoke_test_npm_package.yaml b/.github/workflows/_smoke_test_npm_package.yaml new file mode 100644 index 00000000..693b055c --- /dev/null +++ b/.github/workflows/_smoke_test_npm_package.yaml @@ -0,0 +1,50 @@ +name: Smoke test npm package + +on: + workflow_call: + inputs: + npm_tag: + description: NPM dist-tag to install (e.g. "beta", "latest"). Ignored when version is set. + required: false + type: string + default: latest + version: + description: Exact version to install (e.g. "1.2.3"). Takes precedence over npm_tag. + required: false + type: string + default: "" + +jobs: + smoke_test: + name: Smoke test published package + runs-on: ubuntu-latest + steps: + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 22 + + - name: Install published package + uses: nick-fields/retry@v4 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 30 + command: | + mkdir -p smoke-test && cd smoke-test + npm init -y + VERSION="${{ inputs.version || inputs.npm_tag }}" + npm install "@apify/actors-mcp-server@${VERSION}" @apify/mcpc + + - name: Smoke test MCP server via mcpc + working-directory: smoke-test + run: | + echo '{"mcpServers":{"dev":{"command":"node","args":["node_modules/@apify/actors-mcp-server/dist/stdio.js","--tools","docs,search-actors,fetch-actor-details","--telemetry-enabled","false"]}}}' > .mcp.json + npx mcpc connect .mcp.json:dev @dev + npx mcpc @dev ping + npx mcpc --json @dev tools-list | jq -e 'length > 0' + npx mcpc --json @dev tools-call search-actors keywords:="web scraper" | jq -e '.content[0].text' + npx mcpc --json @dev tools-call fetch-actor-details actor:="apify/web-scraper" | jq -e '.content[0].text' + npx mcpc --json @dev tools-call search-apify-docs query:="web scraping" docSource:="apify" | jq -e '.content[0].text' + npx mcpc --json @dev tools-call fetch-apify-docs url:="https://docs.apify.com/platform/actors" | jq -e '.content[0].text' + npx mcpc @dev close diff --git a/.github/workflows/manual_release_stable.yaml b/.github/workflows/manual_release_stable.yaml index 916a7c12..603d9e06 100644 --- a/.github/workflows/manual_release_stable.yaml +++ b/.github/workflows/manual_release_stable.yaml @@ -119,6 +119,13 @@ jobs: "tag": "latest" } + smoke_test: + name: Smoke test published package + needs: [ release_metadata, publish_to_npm ] + uses: ./.github/workflows/_smoke_test_npm_package.yaml + with: + version: ${{ needs.release_metadata.outputs.version_number }} + bump_dependency_in_internal_repo: name: Bump dependency in apify-mcp-server-internal needs: [ release_metadata, publish_to_npm ] diff --git a/.github/workflows/on_master.yaml b/.github/workflows/on_master.yaml index c9ca9dd6..5e41cc82 100644 --- a/.github/workflows/on_master.yaml +++ b/.github/workflows/on_master.yaml @@ -46,3 +46,10 @@ jobs: "ref": "${{ needs.release_metadata.outputs.changelog_commitish }}", "tag": "beta" } + + smoke_test: + name: Smoke test published package + needs: [ publish_to_npm ] + uses: ./.github/workflows/_smoke_test_npm_package.yaml + with: + npm_tag: beta From a9974e90da43ee10e804177c1449a08e4a1f526e Mon Sep 17 00:00:00 2001 From: Apify Release Bot Date: Tue, 14 Apr 2026 12:44:22 +0000 Subject: [PATCH 5/9] chore(release): Update changelog, package.json, manifest.json and server.json versions [skip ci] --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35971e94..87085f08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,7 @@ All notable changes to this project will be documented in this file. - -## 0.9.18 - **not yet released** +## [0.9.18](https://github.com/apify/apify-mcp-server/releases/tag/v0.9.18) (2026-04-14) ### 🚀 Features @@ -19,7 +18,6 @@ All notable changes to this project will be documented in this file. - Fix error in the validation pipeline ([#669](https://github.com/apify/apify-mcp-server/pull/669)) ([b844f31](https://github.com/apify/apify-mcp-server/commit/b844f31f4ccbae1c53c98d2441a95c1e65799230)) by [@jirispilka](https://github.com/jirispilka) - ## [0.9.17](https://github.com/apify/apify-mcp-server/releases/tag/v0.9.17) (2026-04-08) ### 🚀 Features From 08dc0396a56e6026fb60d913fe7568d71e2571e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Spilka?= Date: Tue, 14 Apr 2026 20:57:24 +0200 Subject: [PATCH 6/9] chore: Use package entry point in smoke test instead of direct JS path (#671) * fix: Use bin entry point in smoke test instead of direct JS path The smoke test was running `node node_modules/.../dist/stdio.js` directly, bypassing the package's bin entry point. Switch to `npx actors-mcp-server` so we actually test the published package as users would use it. Includes a temporary workflow_dispatch copy for testing in PR. Co-Authored-By: Claude Opus 4.6 (1M context) * chore: trigger test workflow on push (delete later) Co-Authored-By: Claude Opus 4.6 (1M context) * fix: Use environment variable for package version in smoke test workflow * chore: remove temp smoke test --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .github/workflows/_smoke_test_npm_package.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/_smoke_test_npm_package.yaml b/.github/workflows/_smoke_test_npm_package.yaml index 693b055c..ad5212ec 100644 --- a/.github/workflows/_smoke_test_npm_package.yaml +++ b/.github/workflows/_smoke_test_npm_package.yaml @@ -26,6 +26,8 @@ jobs: - name: Install published package uses: nick-fields/retry@v4 + env: + PKG_VERSION: ${{ inputs.version || inputs.npm_tag }} with: timeout_minutes: 5 max_attempts: 3 @@ -33,13 +35,12 @@ jobs: command: | mkdir -p smoke-test && cd smoke-test npm init -y - VERSION="${{ inputs.version || inputs.npm_tag }}" - npm install "@apify/actors-mcp-server@${VERSION}" @apify/mcpc + npm install "@apify/actors-mcp-server@${PKG_VERSION}" @apify/mcpc - name: Smoke test MCP server via mcpc working-directory: smoke-test run: | - echo '{"mcpServers":{"dev":{"command":"node","args":["node_modules/@apify/actors-mcp-server/dist/stdio.js","--tools","docs,search-actors,fetch-actor-details","--telemetry-enabled","false"]}}}' > .mcp.json + echo '{"mcpServers":{"dev":{"command":"npx","args":["actors-mcp-server","--tools","docs,search-actors,fetch-actor-details","--telemetry-enabled","false"]}}}' > .mcp.json npx mcpc connect .mcp.json:dev @dev npx mcpc @dev ping npx mcpc --json @dev tools-list | jq -e 'length > 0' From 66f4238fd7ea39a89328c02684017c33f13c4031 Mon Sep 17 00:00:00 2001 From: Apify Release Bot Date: Tue, 14 Apr 2026 18:58:06 +0000 Subject: [PATCH 7/9] chore(release): Update changelog, package.json, manifest.json and server.json versions [skip ci] --- CHANGELOG.md | 5 +++++ manifest.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- server.json | 2 +- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87085f08..3cb47a9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to this project will be documented in this file. + +## 0.9.19 - **not yet released** + + + ## [0.9.18](https://github.com/apify/apify-mcp-server/releases/tag/v0.9.18) (2026-04-14) ### 🚀 Features diff --git a/manifest.json b/manifest.json index b2a1a578..ab28a4f3 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": "0.3", "name": "apify-mcp-server", "display_name": "Apify", - "version": "0.9.18", + "version": "0.9.19", "description": "Extract data from any website with thousands of scrapers, crawlers, and automations on Apify Store.", "long_description": "Apify is the world's largest marketplace of tools for web scraping, crawling, data extraction, and web automation. Get structured data from social media, e-commerce, search engines, maps, travel sites, or any other website.", "keywords": [ diff --git a/package-lock.json b/package-lock.json index a745d2c9..74cffa1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apify/actors-mcp-server", - "version": "0.9.18", + "version": "0.9.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apify/actors-mcp-server", - "version": "0.9.18", + "version": "0.9.19", "license": "MIT", "dependencies": { "@apify/datastructures": "^2.0.3", diff --git a/package.json b/package.json index 212d7e75..5208901d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apify/actors-mcp-server", - "version": "0.9.18", + "version": "0.9.19", "type": "module", "description": "Apify MCP Server", "mcpName": "com.apify/apify-mcp-server", diff --git a/server.json b/server.json index db5dcee2..b1226468 100644 --- a/server.json +++ b/server.json @@ -6,7 +6,7 @@ "url": "https://github.com/apify/apify-mcp-server", "source": "github" }, - "version": "0.9.18", + "version": "0.9.19", "remotes": [ { "type": "streamable-http", From 92acf508a3aa3e305f031aeb26042380e32aa9c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Spilka?= Date: Wed, 15 Apr 2026 09:08:52 +0200 Subject: [PATCH 8/9] chore: Add 't-ai' label to bug report template (#674) --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 65149d06..098b04d3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,7 +1,7 @@ name: Bug report description: Report a bug or issue with the Apify MCP server title: "[Bug]: " -labels: ["bug"] +labels: ["bug","t-ai"] body: - type: dropdown id: server-type From 74eb5dbc25341887aed362af52e2858ad3859dbe Mon Sep 17 00:00:00 2001 From: Jiri Spilka Date: Wed, 15 Apr 2026 15:18:49 +0200 Subject: [PATCH 9/9] test: remove misleading third test case from task.statusMessage tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The third test asserted that a stale intermediate statusMessage is preserved through storeTaskResult — which tests InMemoryTaskStore internals, not the bug. The two remaining cases are sufficient regression coverage. --- tests/unit/task.statusMessage.test.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/tests/unit/task.statusMessage.test.ts b/tests/unit/task.statusMessage.test.ts index 1a86f5f4..e63ceca0 100644 --- a/tests/unit/task.statusMessage.test.ts +++ b/tests/unit/task.statusMessage.test.ts @@ -52,21 +52,4 @@ describe('Task statusMessage after terminal transition', () => { expect(task!.status).toBe('failed'); expect(task!.statusMessage).toBe('my-tool: failed'); }); - - it('should show stale statusMessage when final updateTaskStatus is skipped (documents the bug)', async () => { - const store = new InMemoryTaskStore(); - const taskId = await createWorkingTask(store); - - await store.updateTaskStatus(taskId, 'working', 'Starting the crawler.'); - - // No final updateTaskStatus — this is the buggy path - await store.storeTaskResult(taskId, 'completed', { - content: [{ type: 'text', text: 'result data' }], - }); - - const task = await store.getTask(taskId); - expect(task!.status).toBe('completed'); - // statusMessage is stale — still the intermediate progress message - expect(task!.statusMessage).toBe('Starting the crawler.'); - }); });