Skip to content

Commit 4fdf542

Browse files
committed
refactor: specialize tool gating to report-problem
Only report-problem is conditionally served, so replace the generic tool-gating machinery with a report-problem-specific check. This removes the trap where a second TOOL_CLIENT_BLOCKLIST row would be silently ignored, since gating logic now runs the full chain rather than just the server filter. - server.ts: isToolServable(toolName) -> isReportProblemServable() - const.ts: TOOL_CLIENT_BLOCKLIST record -> REPORT_PROBLEM_BLOCKED_CLIENTS - mcp_clients.ts: isToolBlockedForClient -> isReportProblemBlockedForClient; drop now-unused isClientGatedTool No behavior change.
1 parent 7fa47b4 commit 4fdf542

5 files changed

Lines changed: 43 additions & 60 deletions

File tree

src/const.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,13 @@ export const HELPER_TOOLS = {
5959
export type HelperToolName = (typeof HELPER_TOOLS)[keyof typeof HELPER_TOOLS];
6060

6161
/**
62-
* Tools hidden from specific MCP clients, keyed by tool name → client-name substrings
63-
* (lowercased, matched against `clientInfo.name`). Applied once per connection in the compose
64-
* step, where the client is known. `report-problem` is hidden from Anthropic surfaces
65-
* (Claude.ai / Claude Desktop / Claude Code) pending the directory review. Substring matching
66-
* covers new client builds without a maintained allowlist; over-matching only hides an optional
67-
* tool, which is the safe failure mode.
62+
* Client-name substrings (lowercased, matched against `clientInfo.name`) that `report-problem` is
63+
* hidden from. Applied once per connection in the compose step, where the client is known.
64+
* `report-problem` is hidden from Anthropic surfaces (Claude.ai / Claude Desktop / Claude Code)
65+
* pending the directory review. Substring matching covers new client builds without a maintained
66+
* allowlist; over-matching only hides an optional tool, which is the safe failure mode.
6867
*/
69-
export const TOOL_CLIENT_BLOCKLIST: Record<string, string[]> = {
70-
[HELPER_TOOLS.PROBLEM_REPORT]: ['claude', 'anthropic'],
71-
};
68+
export const REPORT_PROBLEM_BLOCKED_CLIENTS: string[] = ['claude', 'anthropic'];
7269

7370
export const RAG_WEB_BROWSER = 'apify/rag-web-browser';
7471
export const RAG_WEB_BROWSER_WHITELISTED_FIELDS = ['query', 'maxResults', 'outputFormats'];

src/mcp/server.ts

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ import {
9191
computeToolResponseBytes,
9292
getToolCallErrorUserText,
9393
} from '../utils/mcp.js';
94-
import { isClientGatedTool, isToolBlockedForClient } from '../utils/mcp_clients.js';
94+
import { isReportProblemBlockedForClient } from '../utils/mcp_clients.js';
9595
import { buildPaymentRequiredResponse, isX402PaymentRequiredError } from '../utils/payment_errors.js';
9696
import { createProgressTracker } from '../utils/progress.js';
9797
import { getServerInstructions } from '../utils/server-instructions/index.js';
@@ -369,32 +369,34 @@ export class ActorsMcpServer {
369369
}
370370

371371
/**
372-
* Compose the tool list for the current connection: resolve mode-specific tools, then drop any
373-
* not currently servable (see {@link isToolServable}). Client-agnostic tools compose eagerly, so
374-
* a recovery/rehydration load without an initialize still restores them. Client-gated tools are
375-
* withheld until the client is known and re-added by the initialize flush. Used by every
376-
* input-driven load path and the flush. (loadActorsAsTools upserts actor tools directly; actor
377-
* tools are never client-gated, so they need no filtering.)
372+
* Compose the tool list for the current connection: resolve mode-specific tools, then drop
373+
* report-problem unless it is currently servable (see {@link isReportProblemServable}). Every
374+
* other tool composes eagerly, so a recovery/rehydration load without an initialize still
375+
* restores them. report-problem is withheld until the client is known and re-added by the
376+
* initialize flush. Used by every input-driven load path and the flush. (loadActorsAsTools
377+
* upserts actor tools directly; actor tools are never gated, so they need no filtering.)
378378
*/
379379
private composeToolsForClient(input: Input, actorTools: ToolEntry[]): ToolEntry[] {
380-
return getToolsForServerMode(input, actorTools, this.serverMode).filter((tool) =>
381-
this.isToolServable(tool.name),
380+
return getToolsForServerMode(input, actorTools, this.serverMode).filter(
381+
(tool) => tool.name !== HELPER_TOOLS.PROBLEM_REPORT || this.isReportProblemServable(),
382382
);
383383
}
384384

385385
/**
386-
* Whether a tool may be served on this connection right now:
387-
* - report-problem's only function is forwarding submissions via telemetry, so it is never
388-
* servable when telemetry is disabled (it would just fake an acknowledgement into the void).
389-
* - A client-gated tool (has a {@link TOOL_CLIENT_BLOCKLIST} rule) cannot be judged until the
390-
* client is known, so it is withheld until then — the initialize flush re-composes and adds it
391-
* if the client allows. Client-agnostic tools are always servable, so they compose eagerly and
392-
* survive a recovery load that never sees an initialize.
386+
* Whether report-problem may be served on this connection right now:
387+
* - Its only function is forwarding submissions via telemetry, so it is never servable when
388+
* telemetry is disabled (it would just fake an acknowledgement into the void).
389+
* - It cannot be judged until the connecting client is known, so it is withheld until then;
390+
* the initialize flush re-composes and adds it if the client allows.
391+
* Every other tool is unconditionally servable, so recovery loads compose them eagerly and they
392+
* survive a load that never sees an initialize.
393393
*/
394-
private isToolServable(toolName: string): boolean {
395-
if (toolName === HELPER_TOOLS.PROBLEM_REPORT && !this.telemetryEnabled) return false;
396-
if (isClientGatedTool(toolName) && !this.clientKnown) return false;
397-
return !isToolBlockedForClient(toolName, this.options.initializeRequestData);
394+
private isReportProblemServable(): boolean {
395+
return (
396+
!!this.telemetryEnabled &&
397+
this.clientKnown &&
398+
!isReportProblemBlockedForClient(this.options.initializeRequestData)
399+
);
398400
}
399401

400402
private composePendingToolsForClient(): void {
@@ -896,7 +898,7 @@ export class ActorsMcpServer {
896898
const captureResult = <T>(r: T): T => {
897899
// On a failed result, nudge the agent to report the blocker via report-problem at the
898900
// moment it decides what to do next. Gated on the tool actually being served (see
899-
// isToolServable), so clients where it is blocklisted or telemetry is off never see it.
901+
// isReportProblemServable), so clients where it is blocklisted or telemetry is off never see it.
900902
const augmented = appendReportProblemNudge(r, {
901903
failingToolName: resolvedToolName,
902904
available: this.tools.has(HELPER_TOOLS.PROBLEM_REPORT),

src/utils/mcp_clients.ts

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,18 @@
11
import type { InitializeRequest } from '@modelcontextprotocol/sdk/types.js';
22
import { mcpClients } from 'mcp-client-capabilities';
33

4-
import { TOOL_CLIENT_BLOCKLIST } from '../const.js';
4+
import { REPORT_PROBLEM_BLOCKED_CLIENTS } from '../const.js';
55

66
/**
7-
* True when `toolName` is blocklisted for the connecting client (see {@link TOOL_CLIENT_BLOCKLIST}).
8-
* Matches any configured client-name substring against the self-reported `clientInfo.name`
9-
* (lowercased), so new client builds are covered without a maintained allowlist; over-matching
10-
* only hides an optional tool, which is the safe failure mode. An unknown or absent client is
11-
* never blocked.
7+
* True when `report-problem` is blocklisted for the connecting client (see
8+
* {@link REPORT_PROBLEM_BLOCKED_CLIENTS}). Matches any configured client-name substring against the
9+
* self-reported `clientInfo.name` (lowercased), so new client builds are covered without a
10+
* maintained allowlist; over-matching only hides an optional tool, which is the safe failure mode.
11+
* An unknown or absent client is never blocked.
1212
*/
13-
export function isToolBlockedForClient(toolName: string, initializeRequestData?: InitializeRequest): boolean {
14-
const blockedClients = TOOL_CLIENT_BLOCKLIST[toolName];
15-
if (!blockedClients) return false;
13+
export function isReportProblemBlockedForClient(initializeRequestData?: InitializeRequest): boolean {
1614
const clientName = initializeRequestData?.params?.clientInfo?.name?.toLowerCase() ?? '';
17-
return blockedClients.some((blocked) => clientName.includes(blocked));
18-
}
19-
20-
/**
21-
* True when `toolName` has any client-gating rule in {@link TOOL_CLIENT_BLOCKLIST}. Such a tool
22-
* cannot be judged until the connecting client is known, so composition withholds it until then;
23-
* tools with no rule are client-agnostic and compose immediately.
24-
*/
25-
export function isClientGatedTool(toolName: string): boolean {
26-
return TOOL_CLIENT_BLOCKLIST[toolName] !== undefined;
15+
return REPORT_PROBLEM_BLOCKED_CLIENTS.some((blocked) => clientName.includes(blocked));
2716
}
2817

2918
/**

tests/integration/suite.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { assertStatusMessagePropagated, captureInflightActorRunId, waitForRunAbo
2828
const AUTO_INJECTED_TOOL_NAMES = AUTO_INJECTED_TOOLS.map((t) => t.name);
2929

3030
// report-problem is served only when telemetry is enabled — its sole function is forwarding
31-
// submissions via telemetry (see isToolServable in mcp/server.ts). This suite runs with telemetry
31+
// submissions via telemetry (see isReportProblemServable in mcp/server.ts). This suite runs with telemetry
3232
// off (the helper default, so runs emit no Sentry/Segment data), so report-problem is absent from
3333
// the served default set here. Its served/hidden/acknowledge behavior is covered by the unit tests
3434
// (tests/unit/mcp.server.report_problem_gating.test.ts, tests/unit/tools.report_problem.test.ts).

tests/unit/mcp_clients.test.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import type { InitializeRequest } from '@modelcontextprotocol/sdk/types.js';
22
import { describe, expect, it } from 'vitest';
33

4-
import { HELPER_TOOLS } from '../../src/const.js';
5-
import { isToolBlockedForClient } from '../../src/utils/mcp_clients.js';
4+
import { isReportProblemBlockedForClient } from '../../src/utils/mcp_clients.js';
65

76
function initRequest(clientName?: string): InitializeRequest | undefined {
87
if (clientName === undefined) return undefined;
@@ -16,26 +15,22 @@ function initRequest(clientName?: string): InitializeRequest | undefined {
1615
} as InitializeRequest;
1716
}
1817

19-
describe('isToolBlockedForClient', () => {
18+
describe('isReportProblemBlockedForClient', () => {
2019
it.each(['claude-ai', 'claude-code', 'Claude Desktop', 'Anthropic', 'anthropic-sdk'])(
2120
'blocks report-problem for the Anthropic client "%s"',
2221
(clientName) => {
23-
expect(isToolBlockedForClient(HELPER_TOOLS.PROBLEM_REPORT, initRequest(clientName))).toBe(true);
22+
expect(isReportProblemBlockedForClient(initRequest(clientName))).toBe(true);
2423
},
2524
);
2625

2726
it.each(['cursor', 'test-client', 'vscode', ''])(
2827
'serves report-problem to the non-Anthropic client "%s"',
2928
(clientName) => {
30-
expect(isToolBlockedForClient(HELPER_TOOLS.PROBLEM_REPORT, initRequest(clientName))).toBe(false);
29+
expect(isReportProblemBlockedForClient(initRequest(clientName))).toBe(false);
3130
},
3231
);
3332

3433
it('does not block when there is no initialize request data', () => {
35-
expect(isToolBlockedForClient(HELPER_TOOLS.PROBLEM_REPORT, undefined)).toBe(false);
36-
});
37-
38-
it('does not block a tool that is absent from the blocklist, even for an Anthropic client', () => {
39-
expect(isToolBlockedForClient(HELPER_TOOLS.ACTOR_CALL, initRequest('claude-ai'))).toBe(false);
34+
expect(isReportProblemBlockedForClient(undefined)).toBe(false);
4035
});
4136
});

0 commit comments

Comments
 (0)