refactor: Rename 'openai' mode to 'apps' for MCP Apps compatibility#720
refactor: Rename 'openai' mode to 'apps' for MCP Apps compatibility#720jirispilka wants to merge 7 commits intomasterfrom
Conversation
MCP Apps is an open standard; 'openai' was a misleading name. Rename the internal ServerMode and related file paths to 'apps' before any architectural work on top (see #577). - ServerMode = 'default' | 'apps' (was 'default' | 'openai') - Rename src/tools/openai/ -> src/tools/apps/ (preserves git history) - Rename src/utils/server-instructions/openai.ts -> apps.ts - UiMode accepts both 'apps' (canonical) and 'openai' (deprecated alias). resolveServerMode() normalizes; ActorsMcpServer constructor logs a deprecation warning when 'openai' is used. - Rename mode-scoped exports: openaiSearchActors/openaiFetchActorDetails/ openaiCallActor/openaiGetActorRun/openaiActorExecutor -> apps* equivalents. No behavior change. 561 unit tests pass, lint clean, type-check clean. Deprecation alias verified by an existing stdio integration test. Part of #577. Closes #714.
stdio.ts sets log level to ERROR at startup (to avoid leaking log output into the JSON-RPC channel), which silenced log.warning. Switch the deprecation notice to console.warn so it always reaches stderr regardless of the ambient log level. Verified via mcpc probe matrix: - UI_MODE=openai prints "UI mode 'openai' is deprecated; use 'apps' instead." - UI_MODE=apps and UI_MODE=true emit no warning - Both produce identical tool lists and widget metadata Follow-up to #714.
… improved type safety.
…pe to allow `undefined`
There was a problem hiding this comment.
Pull request overview
Renames the server “UI mode” from openai to apps across core codepaths (types, tool categories, instructions, CLI/dev server parsing) and updates unit/integration tests accordingly.
Changes:
- Introduces
ServerModeconstants (default/apps) andparseServerMode()(accepting legacyopenai+true/falseshorthands). - Renames OpenAI-mode tool implementations and wiring to
apps*equivalents and updates instruction selection. - Updates tool-loading logic and tests to use
appsmode terminology/keys.
Reviewed changes
Copilot reviewed 37 out of 37 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/utils.tools_loader.test.ts | Updates loader tests to use apps mode |
| tests/unit/types.parse_server_mode.test.ts | Adds coverage for parseServerMode() mappings |
| tests/unit/tools.search_actors.widget.response.test.ts | Switches widget test import from openai* to apps* |
| tests/unit/tools.mode_contract.test.ts | Renames mode-contract assertions to apps |
| tests/unit/tools.categories.test.ts | Updates category tests for apps mode |
| tests/unit/tools.call_actor_common.test.ts | Renames “openai” expectations to “apps” wording |
| tests/unit/mcp.utils.test.ts | Updates resource tests to use apps mode |
| tests/integration/suite.ts | Updates integration suite mode params + alias coverage |
| tests/helpers.ts | Renames test client option uiMode → serverMode (still passed via ?ui= / UI_MODE) |
| src/utils/tools_loader.ts | Loads UI tools and injects get-actor-run based on apps mode |
| src/utils/tools.ts | Strips widget meta when mode is not apps |
| src/utils/server-instructions/index.ts | Switches instructions mapping from openai → apps |
| src/utils/server-instructions/apps.ts | Renames instructions builder to getAppsInstructions() and updates wording |
| src/utils/actor_details.ts | Updates widget payload comment to “apps variant” |
| src/types.ts | Defines ServerMode constants + parseServerMode(); options now use serverMode |
| src/tools/structured_output_schemas.ts | Updates comment reference from openai/ → apps/ |
| src/tools/index.ts | Uses ServerMode.DEFAULT as default argument and updates default-category resolution |
| src/tools/default/call_actor.ts | Updates comment to reference non-apps stripping |
| src/tools/core/search_actors_common.ts | Renames docs/comments to “apps variants” |
| src/tools/core/get_actor_run_common.ts | Renames docs/comments to “apps variants” |
| src/tools/core/fetch_actor_details_common.ts | Renames docs/comments to “apps variants” |
| src/tools/core/call_actor_common.ts | Renames docs/comments to “apps variants” |
| src/tools/core/actor_tools_factory.ts | Updates comment to reference non-apps stripping |
| src/tools/categories.ts | Renames tool mode maps from openai → apps and updates docs |
| src/tools/apps/search_actors_internal.ts | Updates internal-tool description to “apps mode” |
| src/tools/apps/search_actors.ts | Renames tool export to appsSearchActors |
| src/tools/apps/get_actor_run.ts | Renames tool export to appsGetActorRun |
| src/tools/apps/fetch_actor_details_internal.ts | Updates internal-tool description to “apps mode” |
| src/tools/apps/fetch_actor_details.ts | Renames tool export to appsFetchActorDetails |
| src/tools/apps/call_actor.ts | Renames tool export/description constant to apps* |
| src/tools/apps/actor_executor.ts | Renames executor export to appsActorExecutor |
| src/stdio.ts | Parses --ui/UI_MODE via parseServerMode() and passes serverMode |
| src/resources/resource_service.ts | Serves widgets only when mode is apps |
| src/mcp/utils.ts | Defaults mode to ServerMode.DEFAULT in URL param tool resolution |
| src/mcp/server.ts | Wires appsActorExecutor and normalizes legacy uiMode/openai via parseServerMode() |
| src/index_internals.ts | Removes re-exports of prior mode parsing/types from ./internals |
| src/dev_server.ts | Parses ?ui= / UI_MODE via parseServerMode() and passes serverMode |
Comments suppressed due to low confidence (1)
src/index_internals.ts:44
src/index_internals.tsused to re-export the mode parsing/types (parseUiMode,SERVER_MODES,UiMode,ServerMode), but they were removed without an equivalent replacement. Since./internalsis a published entrypoint (package.json exports), consider re-exportingparseServerMode,SERVER_MODES, andServerMode(and/or keeping deprecated aliases) to avoid a breaking change for consumers.
export {
APIFY_FAVICON_URL,
ApifyClient,
getExpectedToolNamesByCategories,
getServerCard,
TTLLRUCache,
actorNameToToolName,
HelperTools,
SERVER_NAME,
SERVER_TITLE,
defaults,
getDefaultTools,
addTool,
getCategoryTools,
toolCategoriesEnabledByDefault,
type ActorStore,
type ServerCard,
type ToolCategory,
processParamsGetTools,
getActorsAsTools,
getToolPublicFieldOnly,
getUnauthEnabledToolCategories,
unauthEnabledTools,
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -30,7 +30,7 @@ describe('search-actors with widget (openaiSearchActors)', () => { | |||
| it('returns widgetActors, _meta, and OpenAI-specific instructions and text', async () => { | |||
There was a problem hiding this comment.
The test case description still says "OpenAI-specific" even though the suite now targets apps mode. Updating the wording will avoid confusion when grepping for OpenAI-only behavior.
| * @param mode - Required. Use `'default'` or `'apps'`. | ||
| * Made explicit (no default value) to prevent accidentally serving wrong-mode tools. |
There was a problem hiding this comment.
The JSDoc says the mode param is "Made explicit (no default value)", but getCategoryTools currently has a default (ServerMode.DEFAULT). Either remove the default or update the comment so it matches the function signature.
| * @param mode - Required. Use `'default'` or `'apps'`. | |
| * Made explicit (no default value) to prevent accidentally serving wrong-mode tools. | |
| * @param mode - Optional. Use `'default'` or `'apps'`. | |
| * Defaults to `ServerMode.DEFAULT` when omitted. |
| * Defaults to `ServerMode.DEFAULT` when unset. | ||
| */ | ||
| uiMode?: UiMode; | ||
| serverMode?: ServerMode; |
There was a problem hiding this comment.
ActorsMcpServerOptions was changed from uiMode?: UiMode to serverMode?: ServerMode, but the constructor still accepts a legacy uiMode at runtime. This creates a TypeScript-level breaking change for programmatic callers (and contradicts the stated back-compat intent). Consider reintroducing uiMode?: string | ServerMode (marked @deprecated) or a dedicated deprecated UiMode alias so existing consumers compile while the runtime normalization remains.
| serverMode?: ServerMode; | |
| serverMode?: ServerMode; | |
| /** | |
| * @deprecated Use `serverMode` instead. | |
| * Legacy alias kept for TypeScript backward compatibility with callers that still pass `uiMode`. | |
| */ | |
| uiMode?: string | ServerMode; |
| // Constructor is an ingestion boundary for programmatic callers. Normalize via | ||
| // parseServerMode so that runtime-invalid values ('openai' alias, stray strings) | ||
| // and the legacy `uiMode` field name are accepted gracefully during the transition | ||
| // to the canonical `serverMode` API. Remove the `uiMode` fallback once internal | ||
| // consumers have migrated (see apify-mcp-server-internal#454). | ||
| const legacyUiMode = (options as { uiMode?: string }).uiMode; | ||
| this.serverMode = parseServerMode(options.serverMode ?? legacyUiMode) ?? ServerMode.DEFAULT; | ||
| this.actorExecutor = actorExecutorsByMode[this.serverMode]; |
There was a problem hiding this comment.
The code normalizes the deprecated 'openai' value via parseServerMode(...) but does not emit any deprecation warning. If the intended behavior is to warn on 'openai' usage (as described in the PR), add a warning here when options.serverMode/legacy uiMode is 'openai'; otherwise, the PR description should be updated to match the silent normalization behavior.
…, deprecate `uiMode`
* refactor: Extend serverMode option to accept 'auto'
Add a single ServerModeOption type that extends ServerMode with 'auto'
for capability-driven resolution, instead of introducing a parallel
UiOverride tri-state field. parseServerMode now always returns a valid
ServerModeOption ('auto' for missing/unknown input), and a new
resolveServerMode helper folds in client-capability detection.
Prepares for the capability-detection wiring in the next commit. Runtime
behavior unchanged: at construction time clientSupportsUi is false, so
'auto' resolves to DEFAULT — the same effective mode as before.
Behavior change at ingestion boundaries: an unknown raw value (e.g.
UI_MODE=bogus) now maps to 'auto' instead of undefined + DEFAULT
fallback. The effective mode at construction is still DEFAULT, but once
the next commit wires capability detection, typos become auto-detect
instead of silent default.
* feat: Auto-detect MCP Apps capability via initialize request
When `options.serverMode` is `'auto'` (the default), resolve the final
mode from the client's `initialize` capabilities: pick `apps` iff the
client advertises the `io.modelcontextprotocol/ui` extension with the
widget MIME type, otherwise `default`. Explicit `'default'`/`'apps'`
values bypass detection entirely (used by apify-mcp-server-internal).
Tools are finalized BEFORE the InitializeResult is sent, so clients that
ignore `notifications/tools/list_changed` still see the correct tool set
on the first `tools/list`. Entry points register a deferred tools loader
that `prepareForInitialize` runs after the mode is resolved; the stdio
and SSE transports wrap `transport.onmessage` to await that method
before dispatching `initialize`, and the streamable HTTP path awaits it
inline before `connect`.
Also fixes four issues found in review of the prior iteration:
- Unhandled promise rejections in the transport wrappers: the stdio
and SSE `transport.onmessage` proxies now `.catch` async failures,
close the transport, and log — instead of dropping the rejection
on the floor and hanging the client forever.
- Batch JSON-RPC initialize: the streamable HTTP handler extracts the
actual initialize message from array bodies before passing it into
`prepareForInitialize` and `initializeRequestData`, so capability
detection and telemetry don't silently fall back to undefined.
- Resource service frozen to preliminary mode: `createResourceService`
now takes a `getMode` getter instead of a captured `mode` value, so
widget listing and reads pick up the mode flip after
`prepareForInitialize` resolves `'auto'` to `'apps'`.
- Stale initialize instructions: after flipping the mode,
`prepareForInitialize` refreshes the SDK's internal `_instructions`
field so the InitializeResult returns apps-mode instructions (not
the preliminary default-mode ones captured at construction).
* refactor: Remove mode-specific server instructions, unify guidance for all clients
* docs: Update clientSupportsUi JSDoc to reference setupInitializeHandler
Removed references to prepareForInitialize and setupCapabilityNegotiation,
both of which were deleted in 4d2903c when the initialize request handler
override replaced them.
* refactor: Split loadToolsFromInput into async fetch + sync compose
Separate mode-agnostic network work (Actor metadata fetch) from
mode-dependent composition (mode-specific internal variants, auto-injection,
dedup). Exports:
- fetchToolSources(input, client, actorStore?) — async, mode-agnostic
- composeTools(sources, mode) — sync, mode-dependent
- loadToolsFromInput(...) — unchanged signature, now a wrapper calling both
Behavior is unchanged. This sets up callers that want to pay the network
cost before the server mode is known — the composition step can then run
synchronously inside the initialize request handler once mode is resolved
(see src/mcp/server.ts#setupInitializeHandler).
Extracted computeActorNamesToLoad as a mode-agnostic helper: the 'is this
selector an Actor name' decision only depends on whether the selector is a
category name or an internal tool in any mode — both of which are
mode-independent lookups.
* docs: Explain initialize request handler vs oninitialized choice
Add a JSDoc block on setupInitializeHandler documenting why we override the
SDK's initialize *request* handler instead of using `server.oninitialized`.
Key points:
- The SDK does not block inbound requests until notifications/initialized has
been processed; notification handlers dispatch fire-and-forget
(see node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js).
- A tools/list can race past our oninitialized callback even if the body is
synchronous, because tool registration and request dispatch run as
separate microtasks.
- The MCP Apps spec recommends checking client capabilities before
registering UI tools; doing this inside the initialize request/response
cycle is the only approach that guarantees tools are finalized before
InitializeResult is sent and before the first tools/list handler runs.
References:
- MCP Apps spec:
https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx
- MCP base lifecycle:
https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle
* refactor: Use ext-apps SDK getUiCapability instead of local constant
Drop the hardcoded MCP_APPS_EXTENSION_ID string and the hand-rolled
capability lookup in detectClientSupportsUi. Use
`@modelcontextprotocol/ext-apps/server`'s `getUiCapability` helper
instead — it's the canonical way per the MCP Apps spec and already
exported by the ext-apps package we depend on.
Switch tsconfig moduleResolution from "Node" (inherited from
@apify/tsconfig base) to "Bundler" so TypeScript honors ext-apps'
conditional exports for the `./server` subpath. This is a pure
resolver-behavior change; our source already uses `.js` extensions on
relative imports (required by either Node/NodeNext/Bundler).
References:
- ext-apps getUiCapability helper:
https://github.com/modelcontextprotocol/ext-apps/blob/main/src/server/index.ts
- MCP Apps capability negotiation:
https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx
* refactor: Collapse server-instructions/{common,index} into one file
The common.ts/index.ts split dates back to when instructions were mode-specific
(apps.ts + default.ts + common.ts template). After 4d2903c unified them,
index.ts became a one-line wrapper around getCommonInstructions(); the split
added nothing.
Inline the content into index.ts as getServerInstructions() and delete
common.ts. Also clean up two stale JSDoc references to the removed
prepareForInitialize method in stdio.ts and resource_service.ts.
* refactor: Rename composeTools to buildToolsForMode, unify input normalization, and clean up phase workflows
- Replaced `composeTools` with `buildToolsForMode` for clarity.
- Consolidated input normalization logic into `normalizeInput`, improving consistency.
- Renamed `computeActorNamesToLoad` to `resolveActorsToLoad`.
- Revised workflows by separating mode-agnostic and mode-dependent phases, ensuring better readability and maintainability.
* refactor: Replace deferred tools loader with mode-agnostic queuing
- Removed `setDeferredToolsLoader` in favor of queuing mode-agnostic sources via `loadToolsFromInput`, `loadToolsByName`, and `loadToolsFromUrl`.
- Added pending tool source tracking with a single flush point in the `initialize` request handler, enabling resolution of client capabilities before building the final tool set.
- Consolidated duplicate workflows around tool registration.
- Updated tests and documentation to reflect new loading logic.
* refactor: Rename variables and refine workflow for mode resolution
- Renamed `hasResolvedServerMode` to `serverModeResolved` for clarity and consistency.
- Replaced `pendingToolSources` with `pendingToolsAfterModeResolved`, storing only mode-agnostic inputs for improved tool composition.
- Updated `flushPendingToolSources` to `updateToolsAfterServerModeResolved`, aligning with new pending tools structure.
- Adjusted workflows to better handle mode-specific tool composition post-mode resolution.
* feat: Add serverMode configuration and clientCapabilities to MCP server tests
- Introduced `serverMode` configuration to `ActorsMcpServer` initialization in tests, defaulting to `ServerMode.DEFAULT`.
- Enhanced `createClientFn` to support `clientCapabilities` for advertising features during initialization.
- Added new tests for auto-detecting server modes (`apps` vs `default`) based on client UI capabilities.
- Updated helper methods to register client capabilities during connection setup.
- Refined test descriptions in capability gating scenarios for improved clarity.
* refactor: Improve pending tool handling and mode-dependent workflows
- Replace `pendingToolsAfterModeResolved` structure to capture actor tools per input for better tool composition isolation.
- Enhance workflows in `updateToolsAfterServerModeResolved` to fully reconcile mode-specific helpers during the final flush.
- Refactor server mode option parsing to explicitly handle raw and legacy fields.
- Refine widget handling guidance and clarify behavior for clients with/without UI capabilities.
- Update documentation to reflect adjusted workflows and renamed utility methods across phases.
* refactor: Improve widget handling logic and parameter initialization in server tools
- Refined guidance for widget-backed vs silent lookups based on client capabilities and follow-up workflows.
- Fixed parameter initialization in `transport.onmessage` to enforce proper structure for `ApifyRequestParams`.
* refactor: Enhance server instructions to be mode-aware and improve client handling
🔗 PR chain — part of #577
This is step 1 of 6 in the #577 umbrella rollout of the MCP Apps decoupled pattern.
openai→appsfetch-actor-detailssplit (pilot)search-actorssplitcall-actorsplitget-actor-runsplitCloses #714.
Release coordination
apify-mcp-server-internalto migrate twice (once toserverMode, then again touiOverridewhen refactor: Auto-detect MCP Apps capability and gate server mode #721 lands). Ship refactor: Rename 'openai' mode to 'apps' for MCP Apps compatibility #720 + refactor: Auto-detect MCP Apps capability and gate server mode #721 together.#720 + #721combined surface (canonicalserverMode+ tri-stateuiOverride). It will not land until refactor: Auto-detect MCP Apps capability and gate server mode #721 is merged.Summary
Renames the
'openai'server mode to'apps'throughout the codebase to align with the MCP Apps naming convention. The'openai'string is retained as a deprecated alias in the external API (UiMode) for backward compatibility, with automatic normalization to'apps'and a deprecation warning at ingestion.Preparatory refactor — no behavior change. Later PRs in the chain build the decoupled-pattern architecture on top.
Key Changes
Type system & API surface
ServerMode:'default' | 'openai'→'default' | 'apps'UiMode(external):'apps' | 'openai'(both accepted;'openai'normalized with a warning)resolveServerMode(uiMode)insrc/types.tsnormalizesUiMode→ canonicalServerModeparseUiMode()accepts'apps','true'(alias for'apps'),'openai'(deprecated alias)Directory & file renames
src/tools/openai/→src/tools/apps/(all files, imports;git mvpreserves history)src/utils/server-instructions/openai.ts→.../apps.tsopenaiActorExecutor/openaiCallActor/openaiSearchActors/openaiFetchActorDetails/openaiGetActorRun→apps*equivalentsServer
ActorsMcpServerconstructor:console.warnonuiMode === 'openai'(deprecation), then normalizes viaresolveServerMode()console.warnrather thanlog.warning— stdio mode setsloglevel to ERROR at startup, which would silencelog.warning;console.warnalways reaches stderrCategory & executor
toolCategoriesmode keys:openai:→apps:actorExecutorsByMode: uses'apps'Instructions & comments
getOpenaiInstructions→getAppsInstructions, "UI Mode" → "Apps Mode")openai/*_metakeys and their prefix references — those are real OpenAI Apps SDK spec keys (e.g.openai/widgetCSP,openai/outputTemplate) and must stay verbatimTests
'apps''openai'alias path (documented in-test)Test plan
npm run type-checkpassesnpm run lintpassesnpm run test:unitpasses (561/561)mcpcprobe matrix:-internaltools, no widget meta, no deprecation warning--ui=true:-internaltools present, widget meta, no deprecationUI_MODE=apps: same as--ui=trueUI_MODE=openai: same as apps mode + deprecation warning in stderrUI_MODE=bogus: falls back to default modeapify-mcp-server-internal#454lands after refactor: Auto-detect MCP Apps capability and gate server mode #721 merges — do not remove theuiMode/serverModecompat shims before thathttps://claude.ai/code/session_01WDG6wb9JUB9Qb6fYTQ8G7y