Skip to content

Commit 1bf1665

Browse files
jirispilkaclaude
andauthored
refactor: Split tools/utils.ts into naming and input-schema modules (#1040)
Splits `src/tools/utils.ts` into two intention-revealing modules (#1021). A small devforge config alignment also rides along on this branch as a separate commit. ## What we're solving `src/tools/utils.ts` mixed two unrelated concerns behind a vague name — a few tool-name / Actor-predicate helpers and the bulk Actor-input-schema transformation pipeline — so consumers imported the same module for very different reasons. ## How - **`src/tools/actor_tool_naming.ts`** (new): `actorNameToToolName`, `legacyToolNameToNew`, `getToolSchemaID`, `isActorInfoMcpServer`, `isActorBlockedUnderPaymentProvider`. - **`src/tools/actor_input_schema.ts`** (`git mv` from `utils.ts`): the transform / AJV / enum bulk. One-way dependency — it imports `getToolSchemaID` from the naming module; no cycle. - Importers re-pointed across `src/` and tests. The `internals` re-export surface is unchanged, so the hosted server needs no change. Pure structural move — behavior identical. The 999-case unit suite passes unchanged (only test import paths moved, no assertions touched). Closes #1021 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01F9BCu79n9uTdTnGRTHt6SY --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5c7221a commit 1bf1665

16 files changed

Lines changed: 85 additions & 79 deletions

.devforge/config.json

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"stages": {
3-
"validate": { "use": "brainstorming", "model": "opus" },
4-
"architect": { "use": "dig", "model": "opus" },
5-
"implementer": { "use": "feature-dev", "model": "opus" },
3+
"verify_request": { "use": "brainstorming", "model": "opus" },
4+
"architect": { "use": "dig", "model": "opus" },
5+
"implementer": { "use": "feature-dev", "model": "opus" },
66
"reviewers": [
77
{ "use": "staff-review", "model": "sonnet" }
88
],
@@ -12,6 +12,14 @@
1212
{ "use": "mcpc-tester", "model": "sonnet" }
1313
]
1414
},
15+
"oracle": {
16+
"commands": [
17+
"pnpm run type-check",
18+
"pnpm run lint",
19+
"pnpm run test:unit",
20+
"pnpm run check:agents"
21+
]
22+
},
1523
"limits": { "inner_iterations": 3, "final_review_rounds": 2 },
1624
"plan_mode_gate": true
1725
}

src/index_internals.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { processParamsGetTools } from './mcp/utils.js';
88
import { resolvePaymentProvider } from './payments/index.js';
99
import type { PaymentProvider } from './payments/types.js';
1010
import { getServerCard } from './server_card.js';
11+
import { actorNameToToolName } from './tools/actor_tool_naming.js';
1112
import { addActor } from './tools/actors/add_actor.js';
1213
import {
1314
getActorsAsTools,
@@ -17,7 +18,6 @@ import {
1718
toolCategoriesEnabledByDefault,
1819
unauthEnabledTools,
1920
} from './tools/index.js';
20-
import { actorNameToToolName } from './tools/utils.js';
2121
import type { ActorStore, ServerCard, ToolCategory } from './types.js';
2222
import { parseCommaSeparatedList, parseQueryParamList, readJsonFile } from './utils/generic.js';
2323
import { redactSkyfirePayId } from './utils/logging.js';

src/mcp/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ implementations, not by importing from here.
2424

2525
- **Tool names: capped + hash-deduped.** Names are capped at `MAX_TOOL_NAME_LENGTH`;
2626
over-length or colliding names get a `TOOL_NAME_HASH_LENGTH` hash suffix so the
27-
exposed set stays unique within the limit (the hashing is in `../tools/utils.ts`).
27+
exposed set stays unique within the limit (the hashing is in `../tools/actor_tool_naming.ts`).
2828
Never widen the cap — downstream clients depend on it.
2929
- **Proxy server IDs are keyed by URL, not Actor ID.** `getMCPServerID(url)` is
3030
`sha256(url)` sliced to `SERVER_ID_LENGTH`. One Actor can expose both an SSE and a

src/mcp/proxy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
22

33
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
44

5-
import { fixedAjvCompile } from '../tools/utils.js';
5+
import { fixedAjvCompile } from '../tools/actor_input_schema.js';
66
import type { ActorMcpTool, ToolEntry } from '../types.js';
77
import { TOOL_TYPE } from '../types.js';
88
import { ajv } from '../utils/ajv.js';

src/mcp/server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,12 @@ import type { AvailableWidget } from '../resources/widgets.js';
6363
import { resolveAvailableWidgets } from '../resources/widgets.js';
6464
import { getServerInfo } from '../server_card.js';
6565
import { getTelemetryEnv, trackToolCall } from '../telemetry.js';
66+
import { decodeDotPropertyNames } from '../tools/actor_input_schema.js';
67+
import { legacyToolNameToNew } from '../tools/actor_tool_naming.js';
6668
import { actorExecutor } from '../tools/actors/actor_executor.js';
6769
import { buildPermissionApprovalResponse, checkPaymentProviderStandbyConflict } from '../tools/actors/call_actor.js';
6870
import { getActorsAsTools } from '../tools/index.js';
6971
import type { ActorsAsToolsResult } from '../tools/index.js';
70-
import { decodeDotPropertyNames, legacyToolNameToNew } from '../tools/utils.js';
7172
import type {
7273
ActorsMcpServerOptions,
7374
ActorStore,
Lines changed: 2 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
1-
import { createHash } from 'node:crypto';
2-
31
import type { ValidateFunction } from 'ajv';
42
import type Ajv from 'ajv';
53

6-
import log from '@apify/log';
7-
84
import { ACTOR_ENUM_MAX_LENGTH, ACTOR_MAX_DESCRIPTION_LENGTH, RAG_WEB_BROWSER_WHITELISTED_FIELDS } from '../const.js';
95
import { SchemaTooLargeError } from '../errors.js';
10-
import { MAX_TOOL_NAME_LENGTH, TOOL_NAME_HASH_LENGTH } from '../mcp/const.js';
11-
import type { ActorInfo, ActorInputSchema, SchemaProperties } from '../types.js';
6+
import type { ActorInputSchema, SchemaProperties } from '../types.js';
127
import {
138
addGlobsProperties,
149
addKeyValueProperties,
@@ -17,59 +12,10 @@ import {
1712
addRequestListSourcesProperties,
1813
addResourcePickerProperties as addArrayResourcePickerProperties,
1914
} from '../utils/apify_properties.js';
15+
import { getToolSchemaID } from './actor_tool_naming.js';
2016

2117
type ActorInputSchemaProperties = Record<string, SchemaProperties>;
2218

23-
/*
24-
* Checks if the given ActorInfo represents an MCP server Actor.
25-
*/
26-
export function isActorInfoMcpServer(actorInfo: ActorInfo): boolean {
27-
return !!(actorInfo.webServerMcpPath && actorInfo.actor.actorStandby?.isEnabled);
28-
}
29-
30-
/**
31-
* Whether this Actor must be excluded from tool surfaces and rejected on
32-
* `call-actor` when the session uses a third-party payment provider (x402, Skyfire).
33-
* List-time filtering in `getActorsAsTools` and the call-time guard in
34-
* `checkPaymentProviderStandbyConflict` must use this — not MCP URL presence alone.
35-
*/
36-
export function isActorBlockedUnderPaymentProvider(actorInfo: ActorInfo): boolean {
37-
return !!actorInfo.actor.actorStandby?.isEnabled;
38-
}
39-
40-
export function actorNameToToolName(actorFullName: string): string {
41-
const slashIndex = actorFullName.indexOf('/');
42-
if (slashIndex === -1) {
43-
log.warning(`Actor name "${actorFullName}" does not contain a slash — expected format "username/actor-name"`);
44-
}
45-
46-
const username = slashIndex !== -1 ? actorFullName.slice(0, slashIndex) : '';
47-
const actorName = slashIndex !== -1 ? actorFullName.slice(slashIndex + 1) : actorFullName;
48-
const safeUsername = username.replace(/\./g, '-dot-');
49-
const fullName = slashIndex !== -1 ? `${safeUsername}--${actorName}` : actorName;
50-
51-
if (fullName.length <= MAX_TOOL_NAME_LENGTH) {
52-
return fullName;
53-
}
54-
55-
// Truncate and add hash for uniqueness
56-
const hash = createHash('sha256').update(actorFullName).digest('hex').slice(0, TOOL_NAME_HASH_LENGTH);
57-
return `${fullName.slice(0, MAX_TOOL_NAME_LENGTH - TOOL_NAME_HASH_LENGTH - 1)}-${hash}`;
58-
}
59-
60-
/**
61-
* Converts a legacy tool name (apify-slash-rag-web-browser) to the current format (apify--rag-web-browser).
62-
* Returns null if the name doesn't match the legacy pattern.
63-
*/
64-
export function legacyToolNameToNew(name: string): string | null {
65-
if (!name.includes('-slash-')) return null;
66-
return name.replace('-slash-', '--');
67-
}
68-
69-
export function getToolSchemaID(actorName: string): string {
70-
return `https://apify.com/mcp/${actorNameToToolName(actorName)}/schema.json`;
71-
}
72-
7319
// Real Apify Actor input schemas run up to ~140 KB post-transform; cap at 256 KB to bound AJV's
7420
// synchronous codegen so a pathological untrusted schema can't freeze the event loop. Only the
7521
// untrusted callers (actor_tools_factory, mcp/proxy) reach AJV through here; trusted compileSchema()

src/tools/actor_tool_naming.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { createHash } from 'node:crypto';
2+
3+
import log from '@apify/log';
4+
5+
import { MAX_TOOL_NAME_LENGTH, TOOL_NAME_HASH_LENGTH } from '../mcp/const.js';
6+
import type { ActorInfo } from '../types.js';
7+
8+
/*
9+
* Checks if the given ActorInfo represents an MCP server Actor.
10+
*/
11+
export function isActorInfoMcpServer(actorInfo: ActorInfo): boolean {
12+
return !!(actorInfo.webServerMcpPath && actorInfo.actor.actorStandby?.isEnabled);
13+
}
14+
15+
/**
16+
* Whether this Actor must be excluded from tool surfaces and rejected on
17+
* `call-actor` when the session uses a third-party payment provider (x402, Skyfire).
18+
* List-time filtering in `getActorsAsTools` and the call-time guard in
19+
* `checkPaymentProviderStandbyConflict` must use this — not MCP URL presence alone.
20+
*/
21+
export function isActorBlockedUnderPaymentProvider(actorInfo: ActorInfo): boolean {
22+
return !!actorInfo.actor.actorStandby?.isEnabled;
23+
}
24+
25+
export function actorNameToToolName(actorFullName: string): string {
26+
const slashIndex = actorFullName.indexOf('/');
27+
if (slashIndex === -1) {
28+
log.warning(`Actor name "${actorFullName}" does not contain a slash — expected format "username/actor-name"`);
29+
}
30+
31+
const username = slashIndex !== -1 ? actorFullName.slice(0, slashIndex) : '';
32+
const actorName = slashIndex !== -1 ? actorFullName.slice(slashIndex + 1) : actorFullName;
33+
const safeUsername = username.replace(/\./g, '-dot-');
34+
const fullName = slashIndex !== -1 ? `${safeUsername}--${actorName}` : actorName;
35+
36+
if (fullName.length <= MAX_TOOL_NAME_LENGTH) {
37+
return fullName;
38+
}
39+
40+
// Truncate and add hash for uniqueness
41+
const hash = createHash('sha256').update(actorFullName).digest('hex').slice(0, TOOL_NAME_HASH_LENGTH);
42+
return `${fullName.slice(0, MAX_TOOL_NAME_LENGTH - TOOL_NAME_HASH_LENGTH - 1)}-${hash}`;
43+
}
44+
45+
/**
46+
* Converts a legacy tool name (apify-slash-rag-web-browser) to the current format (apify--rag-web-browser).
47+
* Returns null if the name doesn't match the legacy pattern.
48+
*/
49+
export function legacyToolNameToNew(name: string): string | null {
50+
if (!name.includes('-slash-')) return null;
51+
return name.replace('-slash-', '--');
52+
}
53+
54+
export function getToolSchemaID(actorName: string): string {
55+
return `https://apify.com/mcp/${actorNameToToolName(actorName)}/schema.json`;
56+
}

src/tools/actors/actor_tools_factory.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,9 @@ import { getActorDefinitionCached } from '../../utils/actor.js';
2323
import { ajv } from '../../utils/ajv.js';
2424
import { stripQuoteWrappers } from '../../utils/generic.js';
2525
import { logHttpError } from '../../utils/logging.js';
26+
import { buildActorInputSchema, fixedAjvCompile } from '../actor_input_schema.js';
27+
import { actorNameToToolName, isActorBlockedUnderPaymentProvider, isActorInfoMcpServer } from '../actor_tool_naming.js';
2628
import { buildEnrichedDirectActorOutputSchema, actorRunOutputSchema } from '../structured_output_schemas.js';
27-
import {
28-
actorNameToToolName,
29-
buildActorInputSchema,
30-
fixedAjvCompile,
31-
isActorBlockedUnderPaymentProvider,
32-
isActorInfoMcpServer,
33-
} from '../utils.js';
3429
import { CALL_ACTOR_WAIT_SECS_DEFAULT, WAIT_SECS_MAX } from './actor_run_response.js';
3530

3631
/**

src/tools/actors/call_actor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ import { getHttpStatusCode, logHttpError } from '../../utils/logging.js';
3333
import { buildMCPResponse } from '../../utils/mcp.js';
3434
import { classifyFailureCategory, extractAjvErrorDetails, getToolStatusFromError } from '../../utils/tool_status.js';
3535
import { extractActorId } from '../../utils/tools.js';
36+
import { actorNameToToolName, isActorBlockedUnderPaymentProvider } from '../actor_tool_naming.js';
3637
import { buildGetActorRunSuccessResponse } from '../runs/get_actor_run.js';
3738
import { actorRunOutputSchema } from '../structured_output_schemas.js';
38-
import { actorNameToToolName, isActorBlockedUnderPaymentProvider } from '../utils.js';
3939
import {
4040
abortRunOnSignal,
4141
buildStartRunResponse,

src/utils/actor_details.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Build } from 'apify-client';
33
import type { ApifyClient } from '../apify_client.js';
44
import { connectMCPClient } from '../mcp/client.js';
55
import type { PaymentProvider } from '../payments/types.js';
6-
import { filterSchemaProperties, shortenProperties } from '../tools/utils.js';
6+
import { filterSchemaProperties, shortenProperties } from '../tools/actor_input_schema.js';
77
import type { Actor, ActorCardOptions, ActorInputSchema, ActorStoreList, StructuredActorCard } from '../types.js';
88
import { getActorMcpUrlCached } from './actor.js';
99
import { formatActorForWidget, formatActorToActorCard, formatActorToStructuredCard } from './actor_card.js';

0 commit comments

Comments
 (0)