@apify/actors-mcp-server 0.13.0 → 0.13.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/dist/apify_client.js +1 -4
  2. package/dist/apify_client.js.map +1 -1
  3. package/dist/const.d.ts +6 -5
  4. package/dist/const.d.ts.map +1 -1
  5. package/dist/const.js +7 -9
  6. package/dist/const.js.map +1 -1
  7. package/dist/dev_server.d.ts +18 -6
  8. package/dist/dev_server.d.ts.map +1 -1
  9. package/dist/dev_server.js +140 -18
  10. package/dist/dev_server.js.map +1 -1
  11. package/dist/errors.js +1 -4
  12. package/dist/errors.js.map +1 -1
  13. package/dist/index.d.ts +2 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +5 -6
  16. package/dist/index.js.map +1 -1
  17. package/dist/index_internals.js +1 -4
  18. package/dist/index_internals.js.map +1 -1
  19. package/dist/input.js +1 -4
  20. package/dist/input.js.map +1 -1
  21. package/dist/instrument.js +1 -4
  22. package/dist/instrument.js.map +1 -1
  23. package/dist/mcp/actors.js +1 -4
  24. package/dist/mcp/actors.js.map +1 -1
  25. package/dist/mcp/client.js +1 -4
  26. package/dist/mcp/client.js.map +1 -1
  27. package/dist/mcp/client_context.js +1 -4
  28. package/dist/mcp/client_context.js.map +1 -1
  29. package/dist/mcp/const.js +1 -4
  30. package/dist/mcp/const.js.map +1 -1
  31. package/dist/mcp/errors.js +1 -4
  32. package/dist/mcp/errors.js.map +1 -1
  33. package/dist/mcp/legacy_server.d.ts +75 -0
  34. package/dist/mcp/legacy_server.d.ts.map +1 -0
  35. package/dist/mcp/legacy_server.js +560 -0
  36. package/dist/mcp/legacy_server.js.map +1 -0
  37. package/dist/mcp/proxy.js +1 -4
  38. package/dist/mcp/proxy.js.map +1 -1
  39. package/dist/mcp/server.d.ts +110 -86
  40. package/dist/mcp/server.d.ts.map +1 -1
  41. package/dist/mcp/server.js +274 -672
  42. package/dist/mcp/server.js.map +1 -1
  43. package/dist/mcp/stateless_server.d.ts +58 -0
  44. package/dist/mcp/stateless_server.d.ts.map +1 -0
  45. package/dist/mcp/stateless_server.js +332 -0
  46. package/dist/mcp/stateless_server.js.map +1 -0
  47. package/dist/mcp/task_execution.d.ts +15 -8
  48. package/dist/mcp/task_execution.d.ts.map +1 -1
  49. package/dist/mcp/task_execution.js +30 -26
  50. package/dist/mcp/task_execution.js.map +1 -1
  51. package/dist/mcp/tool_call_engine.d.ts +23 -10
  52. package/dist/mcp/tool_call_engine.d.ts.map +1 -1
  53. package/dist/mcp/tool_call_engine.js +36 -30
  54. package/dist/mcp/tool_call_engine.js.map +1 -1
  55. package/dist/mcp/tool_call_error_mapper.js +1 -4
  56. package/dist/mcp/tool_call_error_mapper.js.map +1 -1
  57. package/dist/mcp/tool_call_telemetry.d.ts +4 -4
  58. package/dist/mcp/tool_call_telemetry.d.ts.map +1 -1
  59. package/dist/mcp/tool_call_telemetry.js +6 -9
  60. package/dist/mcp/tool_call_telemetry.js.map +1 -1
  61. package/dist/mcp/tool_dispatch.d.ts +19 -15
  62. package/dist/mcp/tool_dispatch.d.ts.map +1 -1
  63. package/dist/mcp/tool_dispatch.js +20 -21
  64. package/dist/mcp/tool_dispatch.js.map +1 -1
  65. package/dist/mcp/utils.js +1 -4
  66. package/dist/mcp/utils.js.map +1 -1
  67. package/dist/payments/const.js +1 -4
  68. package/dist/payments/const.js.map +1 -1
  69. package/dist/payments/helpers.js +1 -4
  70. package/dist/payments/helpers.js.map +1 -1
  71. package/dist/payments/index.js +1 -4
  72. package/dist/payments/index.js.map +1 -1
  73. package/dist/payments/resolve.js +1 -4
  74. package/dist/payments/resolve.js.map +1 -1
  75. package/dist/payments/skyfire.js +1 -4
  76. package/dist/payments/skyfire.js.map +1 -1
  77. package/dist/payments/types.js +1 -4
  78. package/dist/payments/types.js.map +1 -1
  79. package/dist/payments/x402.js +1 -4
  80. package/dist/payments/x402.js.map +1 -1
  81. package/dist/prompts/index.js +1 -4
  82. package/dist/prompts/index.js.map +1 -1
  83. package/dist/prompts/prompt_service.js +1 -4
  84. package/dist/prompts/prompt_service.js.map +1 -1
  85. package/dist/resources/api_resources.js +1 -4
  86. package/dist/resources/api_resources.js.map +1 -1
  87. package/dist/resources/resource_service.js +1 -4
  88. package/dist/resources/resource_service.js.map +1 -1
  89. package/dist/resources/widgets.js +1 -4
  90. package/dist/resources/widgets.js.map +1 -1
  91. package/dist/server_card.js +1 -4
  92. package/dist/server_card.js.map +1 -1
  93. package/dist/state.js +1 -4
  94. package/dist/state.js.map +1 -1
  95. package/dist/stdio.js +1 -4
  96. package/dist/stdio.js.map +1 -1
  97. package/dist/telemetry.js +1 -4
  98. package/dist/telemetry.js.map +1 -1
  99. package/dist/tools/actor_input_schema.js +1 -4
  100. package/dist/tools/actor_input_schema.js.map +1 -1
  101. package/dist/tools/actor_tool_naming.js +1 -4
  102. package/dist/tools/actor_tool_naming.js.map +1 -1
  103. package/dist/tools/actors/actor_definition.js +1 -4
  104. package/dist/tools/actors/actor_definition.js.map +1 -1
  105. package/dist/tools/actors/actor_executor.js +1 -4
  106. package/dist/tools/actors/actor_executor.js.map +1 -1
  107. package/dist/tools/actors/actor_run_response.js +1 -4
  108. package/dist/tools/actors/actor_run_response.js.map +1 -1
  109. package/dist/tools/actors/actor_tools_factory.js +1 -4
  110. package/dist/tools/actors/actor_tools_factory.js.map +1 -1
  111. package/dist/tools/actors/call_actor.js +2 -5
  112. package/dist/tools/actors/call_actor.js.map +1 -1
  113. package/dist/tools/actors/fetch_actor_details.d.ts.map +1 -1
  114. package/dist/tools/actors/fetch_actor_details.js +5 -8
  115. package/dist/tools/actors/fetch_actor_details.js.map +1 -1
  116. package/dist/tools/actors/search_actors.js +3 -6
  117. package/dist/tools/actors/search_actors.js.map +1 -1
  118. package/dist/tools/dev/report_problem.d.ts.map +1 -1
  119. package/dist/tools/dev/report_problem.js +3 -4
  120. package/dist/tools/dev/report_problem.js.map +1 -1
  121. package/dist/tools/docs/fetch_apify_docs.js +1 -4
  122. package/dist/tools/docs/fetch_apify_docs.js.map +1 -1
  123. package/dist/tools/docs/search_apify_docs.js +1 -4
  124. package/dist/tools/docs/search_apify_docs.js.map +1 -1
  125. package/dist/tools/index.js +1 -4
  126. package/dist/tools/index.js.map +1 -1
  127. package/dist/tools/registry.js +1 -4
  128. package/dist/tools/registry.js.map +1 -1
  129. package/dist/tools/runs/abort_actor_run.js +1 -4
  130. package/dist/tools/runs/abort_actor_run.js.map +1 -1
  131. package/dist/tools/runs/get_actor_run.js +3 -6
  132. package/dist/tools/runs/get_actor_run.js.map +1 -1
  133. package/dist/tools/runs/get_actor_run_list.js +1 -4
  134. package/dist/tools/runs/get_actor_run_list.js.map +1 -1
  135. package/dist/tools/runs/get_actor_run_log.d.ts.map +1 -1
  136. package/dist/tools/runs/get_actor_run_log.js +3 -4
  137. package/dist/tools/runs/get_actor_run_log.js.map +1 -1
  138. package/dist/tools/storage/get_dataset.js +1 -4
  139. package/dist/tools/storage/get_dataset.js.map +1 -1
  140. package/dist/tools/storage/get_dataset_items.js +3 -6
  141. package/dist/tools/storage/get_dataset_items.js.map +1 -1
  142. package/dist/tools/storage/get_dataset_list.js +1 -4
  143. package/dist/tools/storage/get_dataset_list.js.map +1 -1
  144. package/dist/tools/storage/get_dataset_schema.js +1 -4
  145. package/dist/tools/storage/get_dataset_schema.js.map +1 -1
  146. package/dist/tools/storage/get_key_value_store.js +1 -4
  147. package/dist/tools/storage/get_key_value_store.js.map +1 -1
  148. package/dist/tools/storage/get_key_value_store_keys.js +1 -4
  149. package/dist/tools/storage/get_key_value_store_keys.js.map +1 -1
  150. package/dist/tools/storage/get_key_value_store_list.js +1 -4
  151. package/dist/tools/storage/get_key_value_store_list.js.map +1 -1
  152. package/dist/tools/storage/get_key_value_store_record.js +1 -4
  153. package/dist/tools/storage/get_key_value_store_record.js.map +1 -1
  154. package/dist/tools/storage/storage_helpers.d.ts +1 -1
  155. package/dist/tools/storage/storage_helpers.d.ts.map +1 -1
  156. package/dist/tools/storage/storage_helpers.js +1 -4
  157. package/dist/tools/storage/storage_helpers.js.map +1 -1
  158. package/dist/tools/structured_output_schemas.js +1 -4
  159. package/dist/tools/structured_output_schemas.js.map +1 -1
  160. package/dist/tools/widgets/call_actor_widget.js +1 -4
  161. package/dist/tools/widgets/call_actor_widget.js.map +1 -1
  162. package/dist/tools/widgets/fetch_actor_details_widget.js +1 -4
  163. package/dist/tools/widgets/fetch_actor_details_widget.js.map +1 -1
  164. package/dist/tools/widgets/get_actor_run_widget.js +1 -4
  165. package/dist/tools/widgets/get_actor_run_widget.js.map +1 -1
  166. package/dist/tools/widgets/search_actors_widget.js +3 -6
  167. package/dist/tools/widgets/search_actors_widget.js.map +1 -1
  168. package/dist/tsconfig.tsbuildinfo +1 -1
  169. package/dist/types.d.ts +16 -20
  170. package/dist/types.d.ts.map +1 -1
  171. package/dist/types.js +1 -4
  172. package/dist/types.js.map +1 -1
  173. package/dist/utils/actor.js +1 -4
  174. package/dist/utils/actor.js.map +1 -1
  175. package/dist/utils/actor_card.js +1 -4
  176. package/dist/utils/actor_card.js.map +1 -1
  177. package/dist/utils/actor_details.js +1 -4
  178. package/dist/utils/actor_details.js.map +1 -1
  179. package/dist/utils/actor_search.js +1 -4
  180. package/dist/utils/actor_search.js.map +1 -1
  181. package/dist/utils/ajv.js +1 -4
  182. package/dist/utils/ajv.js.map +1 -1
  183. package/dist/utils/apify_docs.js +1 -4
  184. package/dist/utils/apify_docs.js.map +1 -1
  185. package/dist/utils/apify_errors.js +1 -4
  186. package/dist/utils/apify_errors.js.map +1 -1
  187. package/dist/utils/apify_properties.js +1 -4
  188. package/dist/utils/apify_properties.js.map +1 -1
  189. package/dist/utils/auth.js +1 -4
  190. package/dist/utils/auth.js.map +1 -1
  191. package/dist/utils/console_link.js +1 -4
  192. package/dist/utils/console_link.js.map +1 -1
  193. package/dist/utils/encode_text.js +1 -4
  194. package/dist/utils/encode_text.js.map +1 -1
  195. package/dist/utils/generic.js +1 -4
  196. package/dist/utils/generic.js.map +1 -1
  197. package/dist/utils/logging.js +1 -4
  198. package/dist/utils/logging.js.map +1 -1
  199. package/dist/utils/mcp.js +1 -4
  200. package/dist/utils/mcp.js.map +1 -1
  201. package/dist/utils/mcp_clients.js +1 -4
  202. package/dist/utils/mcp_clients.js.map +1 -1
  203. package/dist/utils/payment_errors.js +1 -4
  204. package/dist/utils/payment_errors.js.map +1 -1
  205. package/dist/utils/pricing_info.js +1 -4
  206. package/dist/utils/pricing_info.js.map +1 -1
  207. package/dist/utils/progress.js +1 -4
  208. package/dist/utils/progress.js.map +1 -1
  209. package/dist/utils/schema_generation.js +1 -4
  210. package/dist/utils/schema_generation.js.map +1 -1
  211. package/dist/utils/server-instructions/index.js +1 -4
  212. package/dist/utils/server-instructions/index.js.map +1 -1
  213. package/dist/utils/server_mode.js +1 -4
  214. package/dist/utils/server_mode.js.map +1 -1
  215. package/dist/utils/tool_categories_helpers.js +1 -4
  216. package/dist/utils/tool_categories_helpers.js.map +1 -1
  217. package/dist/utils/tool_status.d.ts +6 -0
  218. package/dist/utils/tool_status.d.ts.map +1 -1
  219. package/dist/utils/tool_status.js +8 -4
  220. package/dist/utils/tool_status.js.map +1 -1
  221. package/dist/utils/tools.js +1 -4
  222. package/dist/utils/tools.js.map +1 -1
  223. package/dist/utils/tools_loader.js +1 -4
  224. package/dist/utils/tools_loader.js.map +1 -1
  225. package/dist/utils/ttl_lru.js +1 -4
  226. package/dist/utils/ttl_lru.js.map +1 -1
  227. package/dist/utils/userid_cache.js +1 -4
  228. package/dist/utils/userid_cache.js.map +1 -1
  229. package/dist/utils/version.js +1 -4
  230. package/dist/utils/version.js.map +1 -1
  231. package/dist/web/dist/actor-run-widget.js +0 -2
  232. package/dist/web/dist/search-actors-widget.js +0 -2
  233. package/manifest.json +1 -1
  234. package/package.json +6 -2
  235. package/server.json +1 -1
@@ -1,98 +1,107 @@
1
1
  /**
2
2
  * Model Context Protocol (MCP) server for Apify Actors
3
3
  */
4
-
5
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="24ee8a78-9505-5c66-bc50-80953252f83f")}catch(e){}}();
6
- import { randomUUID } from 'node:crypto';
7
- import { InMemoryTaskStore } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js';
8
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
9
- import { CallToolRequestSchema, CancelTaskRequestSchema, ErrorCode, GetPromptRequestSchema, GetTaskPayloadRequestSchema, GetTaskRequestSchema, InitializeRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListTasksRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, RELATED_TASK_META_KEY, SetLevelRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
4
+ import { dirname } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
10
6
  import log from '@apify/log';
11
7
  import { parseBooleanOrNull } from '@apify/utilities';
12
8
  import { ApifyClient } from '../apify_client.js';
13
- import { DEFAULT_TELEMETRY_ENABLED, DEFAULT_TELEMETRY_ENV, FAILURE_CATEGORY, HELPER_TOOLS, TOOL_STATUS, } from '../const.js';
9
+ import { DEFAULT_TELEMETRY_ENABLED, DEFAULT_TELEMETRY_ENV, HELPER_TOOLS } from '../const.js';
14
10
  import { prompts } from '../prompts/index.js';
15
11
  import { createPromptService } from '../prompts/prompt_service.js';
16
12
  import { createResourceService } from '../resources/resource_service.js';
17
13
  import { resolveAvailableWidgets } from '../resources/widgets.js';
18
- import { getServerInfo } from '../server_card.js';
19
14
  import { getTelemetryEnv } from '../telemetry.js';
20
- import { withReportProblemNudge } from '../tools/dev/report_problem.js';
21
15
  import { SERVER_MODE, TOOL_TYPE } from '../types.js';
22
- import { isMcpClientFaultMessage, sanitizeMezmoMessage } from '../utils/logging.js';
23
16
  import { getRequestOriginForClient, isReportProblemBlockedForClient } from '../utils/mcp_clients.js';
24
17
  import { getServerInstructions } from '../utils/server-instructions/index.js';
25
18
  import { parseServerMode, resolveServerMode } from '../utils/server_mode.js';
26
- import { buildActorFields, getToolFullName, getToolPublicFieldOnly } from '../utils/tools.js';
27
19
  import { getActors, getToolsForServerMode, toolNamesToInput } from '../utils/tools_loader.js';
28
20
  import { buildMcpClientContext, isUiSupportedByClient } from './client_context.js';
29
- import { LOG_LEVEL_MAP } from './const.js';
30
- import { InternalError, InvalidParamsError } from './errors.js';
31
- import { emitTaskStatusNotification, executeToolAndUpdateTask } from './task_execution.js';
32
- import { buildPreflightFailureOutcome, classifyToolCallError, executeSyncToolCall, prepareToolCall, } from './tool_call_engine.js';
33
- import { logToolCallAndTelemetry, prepareTelemetryData } from './tool_call_telemetry.js';
34
- import { parseInputParamsFromUrl, storeTaskResultOrSkipIfExpired } from './utils.js';
21
+ import { LegacyMcpServer } from './legacy_server.js';
22
+ import { parseInputParamsFromUrl } from './utils.js';
35
23
  /**
36
- * Legacy protocol boundary: project a service's domain error to its v1 `McpError`, copying `message`
37
- * and `data` unchanged so the wire output (code, message, presence of `data`) is byte-identical.
38
- * Non-domain errors pass through untouched.
24
+ * Stable identity of a fetch input, so reloading the same input replaces its retained source rather
25
+ * than adding another (see {@link ActorsMcpServer.toolSources}). Keys are sorted because callers
26
+ * need not agree on property order. The array replacer filters keys at every depth — safe only
27
+ * while `Input` stays flat; a nested object's keys would be silently dropped and inputs collide.
39
28
  */
40
- function toLegacyMcpError(error) {
41
- if (error instanceof InvalidParamsError)
42
- return new McpError(ErrorCode.InvalidParams, error.message, error.data);
43
- if (error instanceof InternalError)
44
- return new McpError(ErrorCode.InternalError, error.message, error.data);
45
- return error;
29
+ function toolSourceKey(input) {
30
+ return JSON.stringify(input, Object.keys(input).sort());
46
31
  }
47
32
  /**
48
- * Create Apify MCP server
33
+ * Read the widget registry from disk. Mode-agnostic, so a successful read is resolved once and
34
+ * shared (see {@link ActorsMcpServer.resolveWidgetsForMode}). Rejects on a failed scan so the
35
+ * caller can tell that apart from a successful empty registry.
36
+ */
37
+ async function resolveServableWidgets() {
38
+ const resolved = await resolveAvailableWidgets(dirname(fileURLToPath(import.meta.url)));
39
+ const readyWidgets = [];
40
+ const missingWidgets = [];
41
+ for (const [uri, widget] of resolved.entries()) {
42
+ if (widget.exists) {
43
+ readyWidgets.push(widget.name);
44
+ }
45
+ else {
46
+ missingWidgets.push(widget.name);
47
+ log.softFail(`Widget file not found: ${widget.jsPath} (widget: ${uri})`);
48
+ }
49
+ }
50
+ if (readyWidgets.length > 0) {
51
+ log.debug('Ready widgets', { widgets: readyWidgets });
52
+ }
53
+ if (missingWidgets.length > 0) {
54
+ log.softFail('Some widgets are not ready', {
55
+ widgets: missingWidgets,
56
+ note: 'These widgets will not be available. Ensure web/dist files are built and included in deployment.',
57
+ });
58
+ }
59
+ return resolved;
60
+ }
61
+ /**
62
+ * The shared-Apify-behavior facade: owns the tool registry + loaders, server-mode resolution,
63
+ * telemetry config, widgets, prompt/resource services, and token/client resolution. Constructs
64
+ * exactly one {@link LegacyMcpServer} (2025-era adapter) and delegates all v1 protocol work to it.
65
+ * Implements {@link StatelessMcpServerHost} for the 2026-07-28 adapter too, but does not construct
66
+ * it — `createStatelessServer` builds one per request from a snapshot this facade hands out.
49
67
  */
50
68
  export class ActorsMcpServer {
69
+ get clientContext() {
70
+ return this._clientContext;
71
+ }
72
+ get serverMode() {
73
+ return this._serverMode;
74
+ }
75
+ /** The instance's own view: what a stateful connection composes and gates against. */
76
+ get servingContext() {
77
+ return { serverMode: this._serverMode, clientContext: this._clientContext };
78
+ }
51
79
  constructor(options = {}) {
52
80
  var _a;
53
- Object.defineProperty(this, "server", {
54
- enumerable: true,
55
- configurable: true,
56
- writable: true,
57
- value: void 0
58
- });
81
+ /**
82
+ * The resolved tool map the instance's own (stateful) connection serves, composed from
83
+ * `toolSources` once the handshake makes mode and client known. A stateless request never
84
+ * reads it — its snapshot re-composes from the sources ({@link createRequestSnapshot}).
85
+ */
59
86
  Object.defineProperty(this, "tools", {
60
87
  enumerable: true,
61
88
  configurable: true,
62
89
  writable: true,
63
90
  value: void 0
64
91
  });
65
- Object.defineProperty(this, "sigintHandler", {
66
- enumerable: true,
67
- configurable: true,
68
- writable: true,
69
- value: void 0
70
- });
71
- Object.defineProperty(this, "currentLogLevel", {
72
- enumerable: true,
73
- configurable: true,
74
- writable: true,
75
- value: 'info'
76
- });
77
92
  Object.defineProperty(this, "options", {
78
93
  enumerable: true,
79
94
  configurable: true,
80
95
  writable: true,
81
96
  value: void 0
82
97
  });
83
- Object.defineProperty(this, "taskStore", {
84
- enumerable: true,
85
- configurable: true,
86
- writable: true,
87
- value: void 0
88
- });
89
98
  Object.defineProperty(this, "actorStore", {
90
99
  enumerable: true,
91
100
  configurable: true,
92
101
  writable: true,
93
102
  value: void 0
94
103
  });
95
- Object.defineProperty(this, "clientContext", {
104
+ Object.defineProperty(this, "_clientContext", {
96
105
  enumerable: true,
97
106
  configurable: true,
98
107
  writable: true,
@@ -100,10 +109,10 @@ export class ActorsMcpServer {
100
109
  });
101
110
  /**
102
111
  * Resolved server mode. Preliminary value at construction (`'auto'` → `DEFAULT`).
103
- * Finalized inside the `initialize` request handler (see constructor) once the
112
+ * Finalized inside the `initialize` request handler (see {@link applyInitialize}) once the
104
113
  * client's capabilities are known. Effectively set-once per connection.
105
114
  */
106
- Object.defineProperty(this, "serverMode", {
115
+ Object.defineProperty(this, "_serverMode", {
107
116
  enumerable: true,
108
117
  configurable: true,
109
118
  writable: true,
@@ -120,10 +129,9 @@ export class ActorsMcpServer {
120
129
  writable: true,
121
130
  value: void 0
122
131
  });
123
- /** True once the server mode is final: at construction for explicit `default`/`apps`, or after
124
- * the initialize handler resolves `'auto'`. Composing before this in `'auto'` mode would use
125
- * the preliminary DEFAULT mode and produce the wrong (non-widget) tool variants, so composition
126
- * waits for it. Distinct from {@link clientKnown}, which only withholds client-gated tools. */
132
+ /** True once the server mode is final (at construction, or after initialize resolves `'auto'`).
133
+ * Composition waits for it — composing earlier in `'auto'` mode would produce the wrong tool
134
+ * variants. Distinct from {@link clientKnown}, which only withholds client-gated tools. */
127
135
  Object.defineProperty(this, "serverModeResolved", {
128
136
  enumerable: true,
129
137
  configurable: true,
@@ -131,17 +139,30 @@ export class ActorsMcpServer {
131
139
  value: void 0
132
140
  });
133
141
  /**
134
- * Tool sources queued until composition is possible. Enqueued when the mode is not yet resolved
135
- * (`'auto'` before initialize), and re-composed by the initialize flush — which is also when the
136
- * client becomes known, so any client-gated tools withheld by an eager compose are added then.
137
- * We capture the exact actor-tool slice fetched for each request so the flush composes every
138
- * entry against *its own* actor list rather than the accumulated union across unrelated requests.
142
+ * Tool sources queued until composition is possible (`'auto'` mode before initialize),
143
+ * re-composed by the initialize flush once mode and client are known. Each entry keeps the
144
+ * exact actor-tool slice fetched for its input, so the flush composes it against its own list.
145
+ * Keyed like {@link toolSources}: a stateless-only facade never drains this queue, so a reload
146
+ * replaces its entry instead of appending, keeping the queue bounded by distinct inputs.
139
147
  */
140
148
  Object.defineProperty(this, "pendingToolsUntilClientKnown", {
141
149
  enumerable: true,
142
150
  configurable: true,
143
151
  writable: true,
144
- value: []
152
+ value: new Map()
153
+ });
154
+ /**
155
+ * The unresolved inputs `tools` is composed from — not a second tool registry. Retained (never
156
+ * drained) so a stateless request, whose identity arrives per request, can compose its own
157
+ * resolved set from the same inputs without touching `tools`. Keyed by input because nothing
158
+ * drains it: a reload replaces its entry instead of appending, bounding the map by distinct
159
+ * inputs instead of growing per reload for the facade's lifetime.
160
+ */
161
+ Object.defineProperty(this, "toolSources", {
162
+ enumerable: true,
163
+ configurable: true,
164
+ writable: true,
165
+ value: new Map()
145
166
  });
146
167
  // Telemetry configuration (resolved from options and env vars, see setupTelemetry)
147
168
  Object.defineProperty(this, "telemetryEnabled", {
@@ -156,6 +177,19 @@ export class ActorsMcpServer {
156
177
  writable: true,
157
178
  value: void 0
158
179
  });
180
+ // Neutral prompt/resource services; the legacy adapter wires SDK handlers to these.
181
+ Object.defineProperty(this, "promptService", {
182
+ enumerable: true,
183
+ configurable: true,
184
+ writable: true,
185
+ value: void 0
186
+ });
187
+ Object.defineProperty(this, "resourceService", {
188
+ enumerable: true,
189
+ configurable: true,
190
+ writable: true,
191
+ value: void 0
192
+ });
159
193
  // List of widgets that are ready to be served
160
194
  Object.defineProperty(this, "availableWidgets", {
161
195
  enumerable: true,
@@ -163,6 +197,16 @@ export class ActorsMcpServer {
163
197
  writable: true,
164
198
  value: new Map()
165
199
  });
200
+ /**
201
+ * In-flight or successfully settled widget resolution, memoized so the disk scan runs once. A
202
+ * failed attempt is dropped rather than kept (see {@link resolveWidgetsForMode}).
203
+ */
204
+ Object.defineProperty(this, "widgetsResolution", {
205
+ enumerable: true,
206
+ configurable: true,
207
+ writable: true,
208
+ value: void 0
209
+ });
166
210
  /** Set in the initialize handler once client capabilities are known. */
167
211
  Object.defineProperty(this, "clientSupportsUi", {
168
212
  enumerable: true,
@@ -170,18 +214,15 @@ export class ActorsMcpServer {
170
214
  writable: true,
171
215
  value: false
172
216
  });
217
+ // The v1 SDK adapter. Package-private: constructed here and never exposed on the public surface.
218
+ Object.defineProperty(this, "legacyServer", {
219
+ enumerable: true,
220
+ configurable: true,
221
+ writable: true,
222
+ value: void 0
223
+ });
173
224
  this.options = options;
174
- this.clientContext = buildMcpClientContext((_a = options.initializeRequestData) === null || _a === void 0 ? void 0 : _a.params);
175
- // for stdio use in memory task store if not provided, otherwise use provided task store
176
- if (this.options.transportType === 'stdio' && !this.options.taskStore) {
177
- this.taskStore = new InMemoryTaskStore();
178
- }
179
- else if (this.options.taskStore) {
180
- this.taskStore = this.options.taskStore;
181
- }
182
- else {
183
- throw new Error('Task store must be provided for non-stdio transport types');
184
- }
225
+ this._clientContext = buildMcpClientContext((_a = options.initializeRequestData) === null || _a === void 0 ? void 0 : _a.params);
185
226
  this.actorStore = options.actorStore;
186
227
  // Constructor is an ingestion boundary for programmatic callers. Normalize via
187
228
  // parseServerMode so that runtime-invalid values ('openai' alias, stray strings)
@@ -194,44 +235,19 @@ export class ActorsMcpServer {
194
235
  rawServerMode !== undefined ? parseServerMode(rawServerMode) : parseServerMode(legacyUiMode);
195
236
  // Preliminary resolution — re-resolved inside the initialize handler once
196
237
  // client capabilities are known (only for 'auto').
197
- this.serverMode = resolveServerMode(this.serverModeOption, false);
238
+ this._serverMode = resolveServerMode(this.serverModeOption, false);
198
239
  this.serverModeResolved = this.serverModeOption !== 'auto';
199
- const { setupSigintHandler = true } = options;
200
- this.server = new Server(getServerInfo(), {
201
- capabilities: {
202
- tools: {
203
- listChanged: true,
204
- },
205
- // Declare long-running task support
206
- tasks: {
207
- list: {},
208
- cancel: {},
209
- requests: {
210
- tools: {
211
- call: {},
212
- },
213
- },
214
- },
215
- // Declared but unused — some clients (e.g. Claude Desktop) fail without it.
216
- resources: {},
217
- prompts: {},
218
- logging: {},
219
- },
220
- instructions: getServerInstructions(),
221
- });
222
240
  const { telemetryEnabled, telemetryEnv } = this.setupTelemetry();
223
241
  this.telemetryEnabled = telemetryEnabled;
224
242
  this.telemetryEnv = telemetryEnv;
225
- this.setupInitializeHandler();
226
- this.setupLoggingProxy();
227
243
  this.tools = new Map();
228
- this.setupErrorHandling(setupSigintHandler);
229
- this.setupLoggingHandlers();
230
- this.setupToolHandlers();
231
- this.setupPromptHandlers();
232
- // Handle resource requests so clients like Claude Desktop don't fail.
233
- this.setupResourceHandlers();
234
- this.setupTaskHandlers();
244
+ this.promptService = createPromptService(prompts);
245
+ this.resourceService = createResourceService({
246
+ paymentProvider: this.options.paymentProvider,
247
+ getMode: () => this.serverMode,
248
+ getAvailableWidgets: () => this.availableWidgets,
249
+ });
250
+ this.legacyServer = new LegacyMcpServer(this);
235
251
  }
236
252
  /**
237
253
  * Telemetry configuration with precedence: explicit options > env vars > defaults
@@ -255,51 +271,83 @@ export class ActorsMcpServer {
255
271
  return { telemetryEnabled, telemetryEnv };
256
272
  }
257
273
  /**
258
- * Override the SDK's `initialize` request handler to run mode resolution and
259
- * pending-source flush before `InitializeResult` is sent. Delegates boilerplate
260
- * (protocolVersion, capabilities, instructions) to the SDK's captured `_oninitialize`.
261
- *
262
- * Not using `server.oninitialized`: the SDK dispatches notification handlers
263
- * fire-and-forget (separate microtask), so a follow-up `tools/list` can race past them.
264
- * The request handler guarantees tools are final before the response and the first `tools/list`.
274
+ * The shared initialize steps the legacy adapter delegates to before returning
275
+ * `InitializeResult`: refresh client context, capture the raw request for session recovery,
276
+ * resolve `'auto'` mode against client capabilities, flush pending tool sources, resolve
277
+ * widgets. Ordering is load-bearing: mode before compose, compose before instructions, so tool
278
+ * presence reflects the final composed set.
265
279
  */
266
- setupInitializeHandler() {
267
- // Capture the SDK's default initialize handler installed in its constructor.
268
- // Private-field access on the SDK Server — verified against
269
- // @modelcontextprotocol/sdk ^1.25.x (see package.json). On SDK bumps, re-check
270
- // `@modelcontextprotocol/sdk/shared/protocol.js` for a still-named `_oninitialize`;
271
- // if renamed or made non-delegable, rebuild the InitializeResult shape here
272
- // (protocolVersion, serverInfo, capabilities, instructions) instead of delegating.
273
- // The capability-gating unit tests construct a server and act as a canary.
274
- // eslint-disable-next-line no-underscore-dangle
275
- const sdkInitHandler = this.server._oninitialize.bind(this.server);
276
- this.server.setRequestHandler(InitializeRequestSchema, async (request) => {
277
- var _a;
278
- this.clientContext = buildMcpClientContext(request.params);
279
- this.options.initializeRequestData = request;
280
- this.clientSupportsUi = isUiSupportedByClient(this.clientContext);
281
- if (this.serverModeOption === 'auto') {
282
- const resolved = resolveServerMode('auto', this.clientSupportsUi);
283
- if (resolved !== this.serverMode) {
284
- this.serverMode = resolved;
285
- }
286
- this.serverModeResolved = true;
280
+ async applyInitialize(request) {
281
+ var _a;
282
+ this._clientContext = buildMcpClientContext(request.params);
283
+ this.options.initializeRequestData = request;
284
+ this.clientSupportsUi = isUiSupportedByClient(this.clientContext);
285
+ if (this.serverModeOption === 'auto') {
286
+ const resolved = resolveServerMode('auto', this.clientSupportsUi);
287
+ if (resolved !== this._serverMode) {
288
+ this._serverMode = resolved;
287
289
  }
288
- log.info('Resolved server mode for client capabilities', {
289
- serverMode: this.serverMode,
290
- serverModeOption: this.serverModeOption,
291
- clientSupportsUi: this.clientSupportsUi,
292
- capabilities: (_a = request === null || request === void 0 ? void 0 : request.params) === null || _a === void 0 ? void 0 : _a.capabilities,
293
- });
294
- this.composePendingToolsForClient();
295
- await this.resolveWidgets();
296
- const result = await sdkInitHandler(request);
297
- // Tools are final here (composePendingToolsForClient ran above, applying the per-client
298
- // blocklist), so tool presence is the ground truth for whether to advertise
299
- // report-problem in the instructions.
300
- result.instructions = getServerInstructions(this.serverMode, this.tools.has(HELPER_TOOLS.PROBLEM_REPORT));
301
- return result;
290
+ this.serverModeResolved = true;
291
+ }
292
+ log.info('Resolved server mode for client capabilities', {
293
+ serverMode: this.serverMode,
294
+ serverModeOption: this.serverModeOption,
295
+ clientSupportsUi: this.clientSupportsUi,
296
+ capabilities: (_a = request === null || request === void 0 ? void 0 : request.params) === null || _a === void 0 ? void 0 : _a.capabilities,
302
297
  });
298
+ this.composePendingToolsForClient();
299
+ await this.resolveInstanceWidgets();
300
+ }
301
+ /**
302
+ * Server instructions for the current connection: mode plus whether report-problem is loaded.
303
+ * Read by the legacy adapter after `applyInitialize`, when the tool set is final.
304
+ */
305
+ getServerInstructions() {
306
+ return getServerInstructions(this.serverMode, this.tools.has(HELPER_TOOLS.PROBLEM_REPORT));
307
+ }
308
+ /**
309
+ * Instructions for a stateless serving unit. The SDK answers `server/discover` from them before
310
+ * any request's envelope is seen, so they are configuration-level: no report-problem mention
311
+ * (that tool's presence is decided per request) and the configured mode only. Reads
312
+ * `serverModeOption`, never `_serverMode` — one facade serves both eras, and a legacy
313
+ * `initialize` rewrites `_serverMode`, which must not leak into later stateless requests.
314
+ */
315
+ getStatelessServerInstructions() {
316
+ return getServerInstructions(resolveServerMode(this.serverModeOption, false));
317
+ }
318
+ /**
319
+ * Build the read-only view one stateless (2026-07-28) request is served from: mode and tool set
320
+ * resolved against *that request's* declared identity, and a resource service bound to both.
321
+ * Nothing request-specific is written back to the facade, so concurrent requests with different
322
+ * identities cannot contaminate each other.
323
+ */
324
+ async createRequestSnapshot(clientContext) {
325
+ // From the configured option, not `_serverMode` — same reason as
326
+ // {@link getStatelessServerInstructions}.
327
+ const serverMode = resolveServerMode(this.serverModeOption, isUiSupportedByClient(clientContext));
328
+ const view = { serverMode, clientContext };
329
+ // Re-compose from the retained sources, not the live `tools` map (composed for the
330
+ // instance's own view). Directly upserted tools are deliberately left out — carrying them
331
+ // over would re-add tools this view's gating just withheld.
332
+ const tools = new Map();
333
+ for (const source of this.toolSources.values()) {
334
+ for (const tool of this.composeToolsForClient(source, view)) {
335
+ const stored = this.toStoredTool(tool);
336
+ tools.set(stored.name, stored);
337
+ }
338
+ }
339
+ const availableWidgets = await this.resolveWidgetsForMode(serverMode);
340
+ return {
341
+ serverMode,
342
+ clientContext,
343
+ tools,
344
+ resourceService: createResourceService({
345
+ paymentProvider: this.options.paymentProvider,
346
+ getMode: () => serverMode,
347
+ getAvailableWidgets: () => availableWidgets,
348
+ }),
349
+ createApifyClient: (token) => this.createApifyClient(token, clientContext),
350
+ };
303
351
  }
304
352
  /** True once the connecting client is known (set in the initialize handler, or hydrated by a
305
353
  * recovery path). Only client-gated tools wait for this so the per-client blocklist can be
@@ -308,37 +356,34 @@ export class ActorsMcpServer {
308
356
  return this.clientContext != null;
309
357
  }
310
358
  /**
311
- * Compose the tool list for the current connection: resolve mode-specific tools, then drop
312
- * report-problem unless it is currently servable (see {@link isReportProblemServable}).
313
- * report-problem is a default-injected tool (via tools_loader) rather than a category member,
314
- * gated here by servability. Every other tool composes eagerly, so a recovery/rehydration load
315
- * without an initialize still restores them. report-problem is withheld until the client is known
316
- * and re-added by the initialize flush. Used by every input-driven load path and the flush.
317
- * (Actor tools are never gated, so they need no filtering.)
359
+ * Compose one source's tool list against `view`: resolve mode-specific tools, then drop
360
+ * report-problem unless servable for that view ({@link isReportProblemServable}). Load paths
361
+ * and the initialize flush pass the instance's own {@link servingContext};
362
+ * {@link createRequestSnapshot} passes a view derived from one stateless request.
318
363
  */
319
- composeToolsForClient(input, actorTools) {
320
- const tools = getToolsForServerMode(input, actorTools, this.serverMode);
321
- if (this.isReportProblemServable())
364
+ composeToolsForClient(source, view) {
365
+ const tools = getToolsForServerMode(source.input, source.actorTools, view.serverMode);
366
+ if (this.isReportProblemServable(view))
322
367
  return tools;
323
368
  return tools.filter((tool) => tool.name !== HELPER_TOOLS.PROBLEM_REPORT);
324
369
  }
325
370
  /**
326
- * Whether report-problem may be served on this connection right now:
327
- * - Its only function is forwarding submissions via telemetry, so it is never servable when
328
- * telemetry is disabled (it would just fake an acknowledgement into the void).
329
- * - It cannot be judged until the connecting client is known, so it is withheld until then;
330
- * the initialize flush re-composes and adds it if the client allows.
331
- * Every other tool is unconditionally servable, so recovery loads compose them eagerly and they
332
- * survive a load that never sees an initialize.
371
+ * Whether report-problem may be served against `view`. Never without telemetry (submissions
372
+ * would vanish into the void) and never before a client context exists — on a stateful
373
+ * connection the initialize flush re-adds it once the handshake supplies one.
374
+ *
375
+ * The stateless envelope requires protocol and capability metadata but not `clientInfo`. A
376
+ * request declaring no client name matches no blocked substring and is served the tool by
377
+ * policy.
333
378
  */
334
- isReportProblemServable() {
335
- return this.telemetryEnabled && this.clientKnown && !isReportProblemBlockedForClient(this.clientContext);
379
+ isReportProblemServable(view) {
380
+ return (this.telemetryEnabled && view.clientContext != null && !isReportProblemBlockedForClient(view.clientContext));
336
381
  }
337
382
  composePendingToolsForClient() {
338
- if (this.pendingToolsUntilClientKnown.length === 0)
383
+ if (this.pendingToolsUntilClientKnown.size === 0)
339
384
  return;
340
- const tools = this.pendingToolsUntilClientKnown.flatMap(({ input, actorTools }) => this.composeToolsForClient(input, actorTools));
341
- this.pendingToolsUntilClientKnown = [];
385
+ const tools = [...this.pendingToolsUntilClientKnown.values()].flatMap((source) => this.composeToolsForClient(source, this.servingContext));
386
+ this.pendingToolsUntilClientKnown.clear();
342
387
  // Load paths already upserted the client-agnostic tools pre-init; re-upserting is
343
388
  // idempotent, and this pass adds the client-gated tools (e.g. report-problem) now that the
344
389
  // client is known.
@@ -384,25 +429,26 @@ export class ActorsMcpServer {
384
429
  return [...this.listInternalToolNames(), ...this.listActorToolNames(), ...this.listActorMcpServerToolIds()];
385
430
  }
386
431
  /**
387
- * Buffer-or-compose gate shared by the actor-tools loaders. If the server mode isn't resolved
388
- * yet ('auto' before initialize), queue the whole source for `composePendingToolsForClient` and
389
- * (if non-empty) upsert the mode-agnostic actor tools immediately.
390
- * Once the mode is resolved, compose the client-specific set via `composeToolsForClient` (which
391
- * withholds report-problem until the client is known) and upsert it; if the client still isn't
392
- * known, queue the source so the initialize flush re-composes and adds the client-gated tools.
432
+ * Buffer-or-compose gate shared by the actor-tools loaders. Mode not resolved yet: queue the
433
+ * source for the initialize flush, upserting the mode-agnostic actor tools immediately. Mode
434
+ * resolved: compose and upsert now; if the client is still unknown, also queue the source so
435
+ * the flush adds the client-gated tools.
393
436
  */
394
437
  registerFetchedActorTools(input, actorTools) {
438
+ const source = { input, actorTools };
439
+ const key = toolSourceKey(input);
440
+ this.toolSources.set(key, source);
395
441
  if (!this.serverModeResolved) {
396
- this.pendingToolsUntilClientKnown.push({ input, actorTools });
442
+ this.pendingToolsUntilClientKnown.set(key, source);
397
443
  if (actorTools.length > 0)
398
444
  this.upsertTools(actorTools);
399
445
  return;
400
446
  }
401
- const tools = this.composeToolsForClient(input, actorTools);
447
+ const tools = this.composeToolsForClient(source, this.servingContext);
402
448
  if (tools.length > 0)
403
449
  this.upsertTools(tools);
404
450
  if (!this.clientKnown)
405
- this.pendingToolsUntilClientKnown.push({ input, actorTools });
451
+ this.pendingToolsUntilClientKnown.set(key, source);
406
452
  }
407
453
  /**
408
454
  * Loads missing toolNames from a provided list of tool names.
@@ -431,14 +477,9 @@ export class ActorsMcpServer {
431
477
  this.registerFetchedActorTools(input, actorTools);
432
478
  }
433
479
  /**
434
- * Two-phase: getActors (async, client-agnostic Apify fetch) then composeToolsForClient
435
- * (sync compose + servability filter). If the mode isn't resolved yet ('auto' before initialize)
436
- * the whole source is queued for the flush. Otherwise tools compose immediately; client-gated
437
- * tools are withheld until the client is known, and the source is queued so the flush adds them.
438
- *
439
- * Don't move the getActors await into the initialize handler — clients time out
440
- * waiting for InitializeResult. The queue buffers already-fetched data, not network
441
- * work. See #721.
480
+ * Two-phase: getActors (async, client-agnostic fetch) then the buffer-or-compose gate.
481
+ * Don't move the getActors await into the initialize handler — clients time out waiting for
482
+ * InitializeResult; the queue buffers already-fetched data, not network work. See #721.
442
483
  */
443
484
  async loadToolsFromInput(input, apifyClient) {
444
485
  const actorTools = await getActors(input, apifyClient, {
@@ -447,86 +488,22 @@ export class ActorsMcpServer {
447
488
  });
448
489
  this.registerFetchedActorTools(input, actorTools);
449
490
  }
450
- /** Delete tools from the server. */
451
- removeToolsByName(toolNames) {
452
- const removedTools = [];
453
- for (const toolName of toolNames) {
454
- if (this.removeToolByName(toolName)) {
455
- removedTools.push(toolName);
456
- }
457
- }
458
- return removedTools;
459
- }
460
491
  /**
461
- * Upsert new tools.
462
- * @param tools - Array of tool wrappers to add or update
463
- * @returns Array of added/updated tool wrappers
492
+ * Upsert new tools. Writes the shared tool map directly, bypassing the retained load sources,
493
+ * so a tool added only this way reaches no stateless snapshot. Load through `loadToolsFrom*` /
494
+ * `loadToolsByName` instead to serve a tool on both protocol eras.
464
495
  */
465
496
  upsertTools(tools) {
466
- // Client gating (e.g. hiding report-problem from Anthropic surfaces) is applied earlier, in
467
- // composeToolsForClient — the single compose choke point where the client is known. Do not
468
- // filter here: this is a low-level commit point reached before the client is known too.
497
+ // Client gating happens earlier, in composeToolsForClient. Do not filter here: this is a
498
+ // low-level commit point reached before the client is known too.
469
499
  for (const tool of tools) {
470
- const stored = this.options.paymentProvider ? this.options.paymentProvider.decorateToolSchema(tool) : tool;
500
+ const stored = this.toStoredTool(tool);
471
501
  this.tools.set(stored.name, stored);
472
502
  }
473
503
  return tools;
474
504
  }
475
- removeToolByName(toolName) {
476
- if (this.tools.has(toolName)) {
477
- this.tools.delete(toolName);
478
- log.debug('Deleted tool', { toolName });
479
- return true;
480
- }
481
- return false;
482
- }
483
- setupErrorHandling(setupSIGINTHandler = true) {
484
- this.server.onerror = (error) => {
485
- var _a;
486
- // Known client faults are expected noise, not server bugs — softFail so they don't
487
- // flood Mezmo error alerts. The fault patterns live in utils/logging.ts.
488
- const message = (_a = error.message) !== null && _a !== void 0 ? _a : '';
489
- if (isMcpClientFaultMessage(message)) {
490
- // Sanitize the errMessage value to preserve the soft-fail level (Mezmo promotes
491
- // entries whose message contains "error").
492
- log.softFail('MCP client fault, request could not be handled', {
493
- errMessage: sanitizeMezmoMessage(message),
494
- });
495
- }
496
- else {
497
- log.error('[MCP Error]', { error });
498
- }
499
- };
500
- if (setupSIGINTHandler) {
501
- const handler = async () => {
502
- await this.server.close();
503
- process.exit(0);
504
- };
505
- process.once('SIGINT', handler);
506
- this.sigintHandler = handler;
507
- }
508
- }
509
- setupLoggingProxy() {
510
- const originalSendLoggingMessage = this.server.sendLoggingMessage.bind(this.server);
511
- // Filter outgoing log messages below the client's requested level.
512
- this.server.sendLoggingMessage = async (params) => {
513
- var _a, _b;
514
- const messageLevelValue = (_a = LOG_LEVEL_MAP[params.level]) !== null && _a !== void 0 ? _a : -1; // Unknown levels get -1, discard
515
- const currentLevelValue = (_b = LOG_LEVEL_MAP[this.currentLogLevel]) !== null && _b !== void 0 ? _b : LOG_LEVEL_MAP.info; // Default to info if invalid
516
- if (messageLevelValue >= currentLevelValue) {
517
- await originalSendLoggingMessage(params);
518
- }
519
- };
520
- }
521
- setupLoggingHandlers() {
522
- this.server.setRequestHandler(SetLevelRequestSchema, (request) => {
523
- const { level } = request.params;
524
- if (LOG_LEVEL_MAP[level] !== undefined) {
525
- this.currentLogLevel = level;
526
- }
527
- // Sending empty result based on MCP spec
528
- return {};
529
- });
505
+ toStoredTool(tool) {
506
+ return this.options.paymentProvider ? this.options.paymentProvider.decorateToolSchema(tool) : tool;
530
507
  }
531
508
  /**
532
509
  * Token sources in order: per-request `_meta.apifyToken` (stdio inline) > server-instance
@@ -544,429 +521,54 @@ export class ActorsMcpServer {
544
521
  * Still carries the request-origin tag from the client context captured by this point.
545
522
  */
546
523
  resolveApifyClient(params) {
547
- const token = this.resolveApifyToken(params._meta);
548
- return token
549
- ? new ApifyClient({ token, requestOrigin: getRequestOriginForClient(this.clientContext) })
550
- : undefined;
524
+ return this.createApifyClient(this.resolveApifyToken(params._meta), this.clientContext);
551
525
  }
552
- setupResourceHandlers() {
553
- const resourceService = createResourceService({
554
- paymentProvider: this.options.paymentProvider,
555
- getMode: () => this.serverMode,
556
- getAvailableWidgets: () => this.availableWidgets,
557
- });
558
- this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
559
- return await resourceService.listResources();
560
- });
561
- this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
562
- try {
563
- return await resourceService.readResource(request.params.uri, this.resolveApifyClient(request.params));
564
- }
565
- catch (error) {
566
- throw toLegacyMcpError(error);
567
- }
568
- });
569
- this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
570
- return await resourceService.listResourceTemplates();
571
- });
526
+ /** The one place a request-scoped Apify client is constructed, on either protocol era. */
527
+ createApifyClient(token, clientContext) {
528
+ return token ? new ApifyClient({ token, requestOrigin: getRequestOriginForClient(clientContext) }) : undefined;
572
529
  }
573
530
  /**
574
- * Sets up MCP request handlers for prompts. The prompt service throws domain errors; the
575
- * boundary maps them to `McpError` so wire output is unchanged.
531
+ * Widgets servable in `mode`: none outside apps mode, otherwise the disk registry. A successful
532
+ * scan is memoized per facade (a widget file appearing later is not picked up); a failed one is
533
+ * dropped so the next caller retries. Touches no per-connection state, so a per-request caller
534
+ * cannot disturb a concurrent one.
576
535
  */
577
- setupPromptHandlers() {
578
- const promptService = createPromptService(prompts);
579
- this.server.setRequestHandler(ListPromptsRequestSchema, () => promptService.listPrompts());
580
- this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
581
- try {
582
- return promptService.getPrompt(request.params.name, request.params.arguments);
583
- }
584
- catch (error) {
585
- throw toLegacyMcpError(error);
586
- }
587
- });
588
- }
589
- /**
590
- * Fetches a task by ID, softFail-logging and throwing a client-facing McpError if it doesn't exist.
591
- */
592
- async getTaskOrThrow(taskId, mcpSessionId, logTag) {
593
- const task = await this.taskStore.getTask(taskId, mcpSessionId);
594
- if (!task) {
595
- // Client error (invalid/unknown taskId) — softFail to avoid polluting error logs.
596
- log.softFail(`[${logTag}] Task not found`, { taskId, mcpSessionId, statusCode: 404 });
597
- throw new McpError(ErrorCode.InvalidParams, `Task "${taskId}" not found`);
598
- }
599
- return task;
600
- }
601
- /**
602
- * Sets up MCP request handlers for long-running tasks.
603
- * Each handler reads `_meta.mcpSessionId` (injected at the transport layer) to isolate
604
- * per-session task stores.
605
- */
606
- setupTaskHandlers() {
607
- // List tasks
608
- this.server.setRequestHandler(ListTasksRequestSchema, async (request) => {
609
- var _a;
610
- const params = (request.params || {});
611
- const { cursor } = params;
612
- const mcpSessionId = (_a = params._meta) === null || _a === void 0 ? void 0 : _a.mcpSessionId;
613
- log.debug('[ListTasksRequestSchema] Listing tasks', { mcpSessionId });
614
- const result = await this.taskStore.listTasks(cursor, mcpSessionId);
615
- return { tasks: result.tasks, nextCursor: result.nextCursor };
616
- });
617
- // Get task status
618
- this.server.setRequestHandler(GetTaskRequestSchema, async (request) => {
619
- var _a;
620
- const params = (request.params || {});
621
- const { taskId } = params;
622
- const mcpSessionId = (_a = params._meta) === null || _a === void 0 ? void 0 : _a.mcpSessionId;
623
- log.debug('[GetTaskRequestSchema] Getting task status', { taskId, mcpSessionId });
624
- return await this.getTaskOrThrow(taskId, mcpSessionId, 'GetTaskRequestSchema');
625
- });
626
- // Get task result payload
627
- this.server.setRequestHandler(GetTaskPayloadRequestSchema, async (request) => {
628
- var _a;
629
- const params = (request.params || {});
630
- const { taskId } = params;
631
- const mcpSessionId = (_a = params._meta) === null || _a === void 0 ? void 0 : _a.mcpSessionId;
632
- log.debug('[GetTaskPayloadRequestSchema] Getting task result', { taskId, mcpSessionId });
633
- const task = await this.getTaskOrThrow(taskId, mcpSessionId, 'GetTaskPayloadRequestSchema');
634
- if (task.status !== 'completed' && task.status !== 'failed') {
635
- throw new McpError(ErrorCode.InvalidParams, `Task "${taskId}" is not completed yet. Current status: ${task.status}`);
636
- }
637
- const result = await this.taskStore.getTaskResult(taskId, mcpSessionId);
638
- // taskId is not in the result body — _meta.related-task lets clients correlate them
639
- return {
640
- ...result,
641
- _meta: {
642
- ...result._meta,
643
- [RELATED_TASK_META_KEY]: { taskId },
644
- },
645
- };
646
- });
647
- // Cancel task
648
- this.server.setRequestHandler(CancelTaskRequestSchema, async (request) => {
649
- var _a;
650
- const params = (request.params || {});
651
- const { taskId } = params;
652
- const mcpSessionId = (_a = params._meta) === null || _a === void 0 ? void 0 : _a.mcpSessionId;
653
- log.debug('[CancelTaskRequestSchema] Cancelling task', { taskId, mcpSessionId });
654
- const task = await this.getTaskOrThrow(taskId, mcpSessionId, 'CancelTaskRequestSchema');
655
- if (task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled') {
656
- // Client error (cancel on terminal task) — softFail to avoid polluting error logs.
657
- log.softFail('[CancelTaskRequestSchema] Task already in terminal state', {
658
- taskId,
659
- mcpSessionId,
660
- status: task.status,
661
- statusCode: 409,
662
- });
663
- throw new McpError(ErrorCode.InvalidParams, `Cannot cancel task "${taskId}" with status "${task.status}"`);
664
- }
665
- await this.taskStore.updateTaskStatus(taskId, 'cancelled', 'Cancelled by client', mcpSessionId);
666
- const updatedTask = await this.taskStore.getTask(taskId, mcpSessionId);
667
- log.debug('[CancelTaskRequestSchema] Task cancelled successfully', { taskId, mcpSessionId });
668
- await emitTaskStatusNotification(taskId, mcpSessionId, this.taskStore, this.server);
669
- return updatedTask;
670
- });
671
- }
672
- setupToolHandlers() {
673
- // Handles the request to list tools.
674
- this.server.setRequestHandler(ListToolsRequestSchema, async () => {
675
- const tools = Array.from(this.tools.values()).map((tool) => getToolPublicFieldOnly(tool, {
676
- mode: this.serverMode,
677
- filterWidgetMeta: true,
678
- }));
679
- return { tools };
680
- });
681
- /**
682
- * Handles the request to call a tool. `extra` carries request-scoped helpers such as
683
- * `sendNotification`. Throws {@link McpError} to mirror the MCP SDK's McpServer error codes.
684
- */
685
- this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
686
- var _a, _b, _c;
687
- const params = request.params;
688
- // Keep telemetry on the decoded arguments.
689
- // eslint-disable-next-line prefer-const
690
- let { name, arguments: args, _meta: meta } = params;
691
- const progressToken = meta === null || meta === void 0 ? void 0 : meta.progressToken;
692
- const apifyToken = this.resolveApifyToken(meta);
693
- // Injected upstream; required for long-running tasks — the task store keys on it and
694
- // there is no other channel to pass it.
695
- const mcpSessionId = meta === null || meta === void 0 ? void 0 : meta.mcpSessionId;
696
- if (!mcpSessionId) {
697
- log.error('MCP Session ID is missing in tool call request. This should never happen.');
698
- throw new Error('MCP Session ID is required for tool calls');
699
- }
700
- const startTime = Date.now();
701
- let toolStatus = TOOL_STATUS.SUCCEEDED;
702
- let callDiagnostics = {};
703
- let shouldTrackTelemetry = true;
704
- let resolvedToolName = name;
705
- // Set only on the pre-flight task path — the one task-mode flow whose telemetry rides
706
- // this handler's `finally` — so its `Tool call completed` log line keeps the taskId the
707
- // async path logs via finishTaskTracking.
708
- let preflightTaskId;
709
- // The nudge must be included in the measured result.
710
- let toolResult = null;
711
- // Keep actor context available to the outer catch.
712
- let actorName;
713
- let actorId;
714
- const { clientContext } = this;
715
- // Start with the raw name so early failures still have telemetry.
716
- const { telemetryData, userId } = await prepareTelemetryData({
717
- toolName: name,
718
- mcpSessionId,
719
- apifyToken,
720
- apifyMcpServer: this,
721
- clientContext,
722
- });
723
- try {
724
- const prepared = await prepareToolCall({
725
- apifyMcpServer: this,
726
- apifyToken,
727
- name,
728
- args,
729
- meta,
730
- requestHeaders: (_a = extra.requestInfo) === null || _a === void 0 ? void 0 : _a.headers,
731
- isTaskRequest: Boolean(request.params.task),
732
- mcpSessionId,
733
- telemetryData,
734
- clientContext,
735
- extra,
736
- });
737
- if ('result' in prepared) {
738
- // The engine already classified this post-resolution failure.
739
- resolvedToolName = prepared.resolvedToolName;
740
- args = prepared.decodedArgs;
741
- toolStatus = prepared.toolStatus;
742
- callDiagnostics = prepared.callDiagnostics;
743
- toolResult = prepared.result;
744
- return prepared.result;
745
- }
746
- if ('message' in prepared) {
747
- // Reproduce v1's invalid-params tail and preserve telemetry fields.
748
- resolvedToolName = (_b = prepared.resolvedToolName) !== null && _b !== void 0 ? _b : resolvedToolName;
749
- if (prepared.decodedArgs)
750
- args = prepared.decodedArgs;
751
- toolStatus = prepared.toolStatus;
752
- callDiagnostics = prepared.callDiagnostics;
753
- log.softFail(prepared.message, {
754
- mcpSessionId,
755
- failureCategory: prepared.callDiagnostics.failure_category,
756
- actorName: prepared.callDiagnostics.actor_name,
757
- validationKeyword: prepared.callDiagnostics.validation_keyword,
758
- validationPath: prepared.callDiagnostics.validation_path,
759
- validationMissingProperty: prepared.callDiagnostics.validation_missing_property,
760
- validationAdditionalProperty: prepared.callDiagnostics.validation_additional_property,
761
- ...prepared.logFields,
762
- });
763
- await this.server.sendLoggingMessage({ level: 'error', data: prepared.message });
764
- throw new McpError(ErrorCode.InvalidParams, prepared.message);
765
- }
766
- const { tool, toolArgs, logSafeArgs, apifyClient, standbyRejection, paymentRequiredResult } = prepared;
767
- actorName = prepared.actorName;
768
- actorId = prepared.actorId;
769
- resolvedToolName = getToolFullName(tool);
770
- // Telemetry uses the decoded arguments.
771
- args = prepared.decodedArgs;
772
- // TODO: we should split this huge method into smaller parts as it is slowly getting out of hand
773
- // Handle long-running task request
774
- if (request.params.task) {
775
- const task = await this.taskStore.createTask({
776
- ttl: request.params.task.ttl,
777
- }, `call-tool-${name}-${randomUUID()}`, request, mcpSessionId);
778
- log.debug('Created task for tool execution', {
779
- taskId: task.taskId,
780
- toolName: tool.name,
781
- mcpSessionId,
782
- });
783
- // Pre-flight failure is already known — the outcome needs no work. Resolve the
784
- // task synchronously: store the failure as the terminal `completed` result and emit
785
- // exactly one `completed` status notification (no `updateTaskStatus('working')`, so no
786
- // `working` notification). Standby rejection wins over the generic payment-required
787
- // short-circuit, matching the sync path's precedence. Telemetry rides the handler's
788
- // outer `finally` (shouldTrackTelemetry stays true), firing once with the sync-path
789
- // properties plus the taskId on the log line.
790
- const preflightResult = standbyRejection !== null && standbyRejection !== void 0 ? standbyRejection : paymentRequiredResult;
791
- if (preflightResult) {
792
- const outcome = buildPreflightFailureOutcome(standbyRejection, paymentRequiredResult, actorName, actorId);
793
- toolStatus = outcome.toolStatus;
794
- callDiagnostics = outcome.callDiagnostics;
795
- preflightTaskId = task.taskId;
796
- try {
797
- await storeTaskResultOrSkipIfExpired(this.taskStore, tool.name, task.taskId, 'completed', outcome.result, mcpSessionId);
798
- }
799
- catch (error) {
800
- // A store failure (not expiry) would otherwise fall through to the generic
801
- // catch and return a task-less tool result the client rejects as a
802
- // CreateTaskResult parse error; surface it as a protocol error instead.
803
- // The pre-flight outcome left toolStatus=SOFT_FAIL, but a genuine store
804
- // outage is a hard failure — correct it before throwing so the handler
805
- // `finally` logs FAILED/INTERNAL_ERROR, not the stale pre-flight SOFT_FAIL.
806
- toolStatus = TOOL_STATUS.FAILED;
807
- callDiagnostics = {
808
- failure_category: FAILURE_CATEGORY.INTERNAL_ERROR,
809
- ...buildActorFields(actorName, actorId),
810
- };
811
- throw new McpError(ErrorCode.InternalError, `Failed to store the pre-flight result for task "${task.taskId}": ${error instanceof Error ? error.message : String(error)}`);
812
- }
813
- // Defer so the client sees the CreateTaskResult (and learns the taskId) before
814
- // the terminal status notification — the async path's post-response ordering.
815
- // emitTaskStatusNotification never throws and no-ops if the task expired.
816
- setImmediate(() => {
817
- void emitTaskStatusNotification(task.taskId, mcpSessionId, this.taskStore, this.server);
818
- });
819
- // Measure the nudged result without changing the stored result.
820
- toolResult = withReportProblemNudge({
821
- result: outcome.result,
822
- tools: this.tools,
823
- failingToolName: resolvedToolName,
824
- failureCategory: callDiagnostics.failure_category,
825
- failureHttpStatus: callDiagnostics.failure_http_status,
826
- });
827
- // createTask returned status `working`; synthesize the terminal status instead of
828
- // re-fetching — if the task expired before the result store (the one case
829
- // storeTaskResultOrSkipIfExpired tolerates), a re-fetch would come back empty and a
830
- // `working` fallback would contradict the tasks/get 404 the client sees next.
831
- return { task: { ...task, status: 'completed' } };
832
- }
833
- // Execute the tool asynchronously and update task status
834
- setImmediate(() => {
835
- executeToolAndUpdateTask({
836
- taskId: task.taskId,
837
- tool,
838
- toolArgs,
839
- logSafeArgs,
840
- apifyClient,
841
- apifyToken,
842
- progressToken,
843
- extra,
844
- mcpSessionId,
845
- actorName,
846
- actorId,
847
- apifyMcpServer: this,
848
- clientContext,
849
- }).catch((error) =>
850
- // Benign task-expiry is handled in-method (see the catch block and
851
- // storeTaskResultOrSkipIfExpired); anything reaching here is genuinely unexpected.
852
- log.error('executeToolAndUpdateTask failed unexpectedly', { taskId: task.taskId, error }));
853
- });
854
- // Return the task immediately; execution continues asynchronously
855
- shouldTrackTelemetry = false;
856
- return { task };
857
- }
858
- // Sync path: run the shared dispatch tail and project its neutral outcome by identity.
859
- const outcome = await executeSyncToolCall(prepared, {
860
- apifyMcpServer: this,
861
- apifyToken,
862
- toolName: name,
863
- mcpSessionId,
864
- progressToken,
865
- extra,
866
- });
867
- toolStatus = outcome.toolStatus;
868
- callDiagnostics = outcome.callDiagnostics;
869
- toolResult = outcome.result;
870
- return outcome.result;
871
- }
872
- catch (error) {
873
- // Match v1: classify task-creation failures, but re-throw protocol errors as JSON-RPC.
874
- if (error instanceof McpError) {
875
- throw error;
876
- }
877
- const outcome = classifyToolCallError(error, {
878
- apifyMcpServer: this,
879
- toolName: name,
880
- failingToolName: resolvedToolName,
881
- actorName,
882
- actorId,
883
- isAborted: Boolean((_c = extra.signal) === null || _c === void 0 ? void 0 : _c.aborted),
884
- mcpSessionId,
885
- });
886
- toolStatus = outcome.toolStatus;
887
- callDiagnostics = outcome.callDiagnostics;
888
- toolResult = outcome.result;
889
- return outcome.result;
890
- }
891
- finally {
892
- if (shouldTrackTelemetry) {
893
- logToolCallAndTelemetry({
894
- toolName: resolvedToolName,
895
- mcpSessionId,
896
- toolStatus,
897
- startTime,
898
- telemetryData,
899
- userId,
900
- callDiagnostics,
901
- args,
902
- result: toolResult,
903
- taskId: preflightTaskId,
904
- apifyMcpServer: this,
905
- });
906
- }
907
- }
908
- });
909
- }
910
- /**
911
- * Resolves widgets and determines which ones are ready to be served.
912
- */
913
- async resolveWidgets() {
914
- if (this.serverMode !== SERVER_MODE.APPS) {
915
- return;
916
- }
917
- try {
918
- const { fileURLToPath } = await import('node:url');
919
- const path = await import('node:path');
920
- const filename = fileURLToPath(import.meta.url);
921
- const dirName = path.dirname(filename);
922
- const resolved = await resolveAvailableWidgets(dirName);
923
- this.availableWidgets = resolved;
924
- const readyWidgets = [];
925
- const missingWidgets = [];
926
- for (const [uri, widget] of resolved.entries()) {
927
- if (widget.exists) {
928
- readyWidgets.push(widget.name);
929
- }
930
- else {
931
- missingWidgets.push(widget.name);
932
- log.softFail(`Widget file not found: ${widget.jsPath} (widget: ${uri})`);
933
- }
934
- }
935
- if (readyWidgets.length > 0) {
936
- log.debug('Ready widgets', { widgets: readyWidgets });
937
- }
938
- if (missingWidgets.length > 0) {
939
- log.softFail('Some widgets are not ready', {
940
- widgets: missingWidgets,
941
- note: 'These widgets will not be available. Ensure web/dist files are built and included in deployment.',
942
- });
943
- }
536
+ async resolveWidgetsForMode(mode) {
537
+ var _a;
538
+ if (mode !== SERVER_MODE.APPS) {
539
+ return new Map();
944
540
  }
945
- catch (error) {
541
+ // Catch on the shared attempt, not per awaiter: N callers awaiting one rejected scan would
542
+ // otherwise report one root cause N times. Dropping the memo lets the next caller re-run it.
543
+ const resolution = ((_a = this.widgetsResolution) !== null && _a !== void 0 ? _a : (this.widgetsResolution = resolveServableWidgets().catch((error) => {
544
+ this.widgetsResolution = undefined;
946
545
  const errorMessage = error instanceof Error ? error.message : String(error);
947
546
  log.softFail(`Failed to resolve widgets: ${errorMessage}`);
948
547
  // Continue without widgets
949
- }
548
+ return new Map();
549
+ })));
550
+ return await resolution;
551
+ }
552
+ /**
553
+ * Resolve the instance's own widget map for this connection's mode. The only writer of that
554
+ * field — per-request callers take the map `resolveWidgetsForMode` returns instead.
555
+ */
556
+ async resolveInstanceWidgets() {
557
+ this.availableWidgets = await this.resolveWidgetsForMode(this.serverMode);
950
558
  }
951
559
  async connect(transport) {
952
- await this.resolveWidgets();
953
- await this.server.connect(transport);
560
+ await this.resolveInstanceWidgets();
561
+ await this.legacyServer.connect(transport);
954
562
  }
955
563
  async close() {
956
- if (this.sigintHandler) {
957
- process.removeListener('SIGINT', this.sigintHandler);
958
- this.sigintHandler = undefined;
959
- }
960
- // Clear all tools and null their compiled schemas.
961
- for (const tool of this.tools.values()) {
962
- if (tool.ajvValidate && typeof tool.ajvValidate === 'function') {
963
- tool.ajvValidate = null;
964
- }
965
- }
564
+ // Transport/server down first, then everything this facade retains: the composed map plus
565
+ // both source maps, each holding every fetched `ToolEntry` with its compiled AJV validator.
566
+ // Close is the release point — a long-lived host churning sessions must not accumulate them
567
+ // — so a stateless snapshot taken after close composes no tools.
568
+ await this.legacyServer.close();
966
569
  this.tools.clear();
967
- // Closing the server also removes its event handlers.
968
- await this.server.close();
570
+ this.toolSources.clear();
571
+ this.pendingToolsUntilClientKnown.clear();
969
572
  }
970
573
  }
971
- //# sourceMappingURL=server.js.map
972
- //# debugId=24ee8a78-9505-5c66-bc50-80953252f83f
574
+ //# sourceMappingURL=server.js.map