@apify/actors-mcp-server 0.12.1-beta.3 → 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.
- package/dist/const.d.ts +6 -5
- package/dist/const.d.ts.map +1 -1
- package/dist/const.js +6 -5
- package/dist/const.js.map +1 -1
- package/dist/dev_server.d.ts +18 -6
- package/dist/dev_server.d.ts.map +1 -1
- package/dist/dev_server.js +139 -14
- package/dist/dev_server.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/legacy_server.d.ts +75 -0
- package/dist/mcp/legacy_server.d.ts.map +1 -0
- package/dist/mcp/legacy_server.js +560 -0
- package/dist/mcp/legacy_server.js.map +1 -0
- package/dist/mcp/server.d.ts +110 -86
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +273 -668
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/stateless_server.d.ts +58 -0
- package/dist/mcp/stateless_server.d.ts.map +1 -0
- package/dist/mcp/stateless_server.js +332 -0
- package/dist/mcp/stateless_server.js.map +1 -0
- package/dist/mcp/task_execution.d.ts +15 -8
- package/dist/mcp/task_execution.d.ts.map +1 -1
- package/dist/mcp/task_execution.js +29 -22
- package/dist/mcp/task_execution.js.map +1 -1
- package/dist/mcp/tool_call_engine.d.ts +23 -10
- package/dist/mcp/tool_call_engine.d.ts.map +1 -1
- package/dist/mcp/tool_call_engine.js +35 -26
- package/dist/mcp/tool_call_engine.js.map +1 -1
- package/dist/mcp/tool_call_telemetry.d.ts +4 -4
- package/dist/mcp/tool_call_telemetry.d.ts.map +1 -1
- package/dist/mcp/tool_call_telemetry.js +5 -5
- package/dist/mcp/tool_call_telemetry.js.map +1 -1
- package/dist/mcp/tool_dispatch.d.ts +19 -15
- package/dist/mcp/tool_dispatch.d.ts.map +1 -1
- package/dist/mcp/tool_dispatch.js +19 -17
- package/dist/mcp/tool_dispatch.js.map +1 -1
- package/dist/tools/actors/call_actor.js +1 -1
- package/dist/tools/actors/call_actor.js.map +1 -1
- package/dist/tools/actors/fetch_actor_details.d.ts.map +1 -1
- package/dist/tools/actors/fetch_actor_details.js +4 -4
- package/dist/tools/actors/fetch_actor_details.js.map +1 -1
- package/dist/tools/actors/search_actors.js +2 -2
- package/dist/tools/actors/search_actors.js.map +1 -1
- package/dist/tools/dev/report_problem.d.ts.map +1 -1
- package/dist/tools/dev/report_problem.js +2 -0
- package/dist/tools/dev/report_problem.js.map +1 -1
- package/dist/tools/runs/get_actor_run.js +2 -2
- package/dist/tools/runs/get_actor_run.js.map +1 -1
- package/dist/tools/runs/get_actor_run_log.d.ts.map +1 -1
- package/dist/tools/runs/get_actor_run_log.js +2 -0
- package/dist/tools/runs/get_actor_run_log.js.map +1 -1
- package/dist/tools/storage/get_dataset_items.js +2 -2
- package/dist/tools/storage/get_dataset_items.js.map +1 -1
- package/dist/tools/storage/storage_helpers.d.ts +1 -1
- package/dist/tools/storage/storage_helpers.d.ts.map +1 -1
- package/dist/tools/storage/storage_helpers.js.map +1 -1
- package/dist/tools/widgets/search_actors_widget.js +2 -2
- package/dist/tools/widgets/search_actors_widget.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +16 -20
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/tool_status.d.ts +6 -0
- package/dist/utils/tool_status.d.ts.map +1 -1
- package/dist/utils/tool_status.js +7 -0
- package/dist/utils/tool_status.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +6 -2
- package/server.json +1 -1
package/dist/mcp/server.js
CHANGED
|
@@ -1,96 +1,107 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Model Context Protocol (MCP) server for Apify Actors
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
7
|
-
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';
|
|
8
6
|
import log from '@apify/log';
|
|
9
7
|
import { parseBooleanOrNull } from '@apify/utilities';
|
|
10
8
|
import { ApifyClient } from '../apify_client.js';
|
|
11
|
-
import { DEFAULT_TELEMETRY_ENABLED, DEFAULT_TELEMETRY_ENV,
|
|
9
|
+
import { DEFAULT_TELEMETRY_ENABLED, DEFAULT_TELEMETRY_ENV, HELPER_TOOLS } from '../const.js';
|
|
12
10
|
import { prompts } from '../prompts/index.js';
|
|
13
11
|
import { createPromptService } from '../prompts/prompt_service.js';
|
|
14
12
|
import { createResourceService } from '../resources/resource_service.js';
|
|
15
13
|
import { resolveAvailableWidgets } from '../resources/widgets.js';
|
|
16
|
-
import { getServerInfo } from '../server_card.js';
|
|
17
14
|
import { getTelemetryEnv } from '../telemetry.js';
|
|
18
|
-
import { withReportProblemNudge } from '../tools/dev/report_problem.js';
|
|
19
15
|
import { SERVER_MODE, TOOL_TYPE } from '../types.js';
|
|
20
|
-
import { isMcpClientFaultMessage, sanitizeMezmoMessage } from '../utils/logging.js';
|
|
21
16
|
import { getRequestOriginForClient, isReportProblemBlockedForClient } from '../utils/mcp_clients.js';
|
|
22
17
|
import { getServerInstructions } from '../utils/server-instructions/index.js';
|
|
23
18
|
import { parseServerMode, resolveServerMode } from '../utils/server_mode.js';
|
|
24
|
-
import { buildActorFields, getToolFullName, getToolPublicFieldOnly } from '../utils/tools.js';
|
|
25
19
|
import { getActors, getToolsForServerMode, toolNamesToInput } from '../utils/tools_loader.js';
|
|
26
20
|
import { buildMcpClientContext, isUiSupportedByClient } from './client_context.js';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import { emitTaskStatusNotification, executeToolAndUpdateTask } from './task_execution.js';
|
|
30
|
-
import { buildPreflightFailureOutcome, classifyToolCallError, executeSyncToolCall, prepareToolCall, } from './tool_call_engine.js';
|
|
31
|
-
import { logToolCallAndTelemetry, prepareTelemetryData } from './tool_call_telemetry.js';
|
|
32
|
-
import { parseInputParamsFromUrl, storeTaskResultOrSkipIfExpired } from './utils.js';
|
|
21
|
+
import { LegacyMcpServer } from './legacy_server.js';
|
|
22
|
+
import { parseInputParamsFromUrl } from './utils.js';
|
|
33
23
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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.
|
|
37
28
|
*/
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
return new McpError(ErrorCode.InvalidParams, error.message, error.data);
|
|
41
|
-
if (error instanceof InternalError)
|
|
42
|
-
return new McpError(ErrorCode.InternalError, error.message, error.data);
|
|
43
|
-
return error;
|
|
29
|
+
function toolSourceKey(input) {
|
|
30
|
+
return JSON.stringify(input, Object.keys(input).sort());
|
|
44
31
|
}
|
|
45
32
|
/**
|
|
46
|
-
*
|
|
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.
|
|
47
67
|
*/
|
|
48
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
|
+
}
|
|
49
79
|
constructor(options = {}) {
|
|
50
80
|
var _a;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
});
|
|
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
|
+
*/
|
|
57
86
|
Object.defineProperty(this, "tools", {
|
|
58
87
|
enumerable: true,
|
|
59
88
|
configurable: true,
|
|
60
89
|
writable: true,
|
|
61
90
|
value: void 0
|
|
62
91
|
});
|
|
63
|
-
Object.defineProperty(this, "sigintHandler", {
|
|
64
|
-
enumerable: true,
|
|
65
|
-
configurable: true,
|
|
66
|
-
writable: true,
|
|
67
|
-
value: void 0
|
|
68
|
-
});
|
|
69
|
-
Object.defineProperty(this, "currentLogLevel", {
|
|
70
|
-
enumerable: true,
|
|
71
|
-
configurable: true,
|
|
72
|
-
writable: true,
|
|
73
|
-
value: 'info'
|
|
74
|
-
});
|
|
75
92
|
Object.defineProperty(this, "options", {
|
|
76
93
|
enumerable: true,
|
|
77
94
|
configurable: true,
|
|
78
95
|
writable: true,
|
|
79
96
|
value: void 0
|
|
80
97
|
});
|
|
81
|
-
Object.defineProperty(this, "taskStore", {
|
|
82
|
-
enumerable: true,
|
|
83
|
-
configurable: true,
|
|
84
|
-
writable: true,
|
|
85
|
-
value: void 0
|
|
86
|
-
});
|
|
87
98
|
Object.defineProperty(this, "actorStore", {
|
|
88
99
|
enumerable: true,
|
|
89
100
|
configurable: true,
|
|
90
101
|
writable: true,
|
|
91
102
|
value: void 0
|
|
92
103
|
});
|
|
93
|
-
Object.defineProperty(this, "
|
|
104
|
+
Object.defineProperty(this, "_clientContext", {
|
|
94
105
|
enumerable: true,
|
|
95
106
|
configurable: true,
|
|
96
107
|
writable: true,
|
|
@@ -98,10 +109,10 @@ export class ActorsMcpServer {
|
|
|
98
109
|
});
|
|
99
110
|
/**
|
|
100
111
|
* Resolved server mode. Preliminary value at construction (`'auto'` → `DEFAULT`).
|
|
101
|
-
* Finalized inside the `initialize` request handler (see
|
|
112
|
+
* Finalized inside the `initialize` request handler (see {@link applyInitialize}) once the
|
|
102
113
|
* client's capabilities are known. Effectively set-once per connection.
|
|
103
114
|
*/
|
|
104
|
-
Object.defineProperty(this, "
|
|
115
|
+
Object.defineProperty(this, "_serverMode", {
|
|
105
116
|
enumerable: true,
|
|
106
117
|
configurable: true,
|
|
107
118
|
writable: true,
|
|
@@ -118,10 +129,9 @@ export class ActorsMcpServer {
|
|
|
118
129
|
writable: true,
|
|
119
130
|
value: void 0
|
|
120
131
|
});
|
|
121
|
-
/** True once the server mode is final
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* 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. */
|
|
125
135
|
Object.defineProperty(this, "serverModeResolved", {
|
|
126
136
|
enumerable: true,
|
|
127
137
|
configurable: true,
|
|
@@ -129,17 +139,30 @@ export class ActorsMcpServer {
|
|
|
129
139
|
value: void 0
|
|
130
140
|
});
|
|
131
141
|
/**
|
|
132
|
-
* Tool sources queued until composition is possible
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
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.
|
|
137
147
|
*/
|
|
138
148
|
Object.defineProperty(this, "pendingToolsUntilClientKnown", {
|
|
139
149
|
enumerable: true,
|
|
140
150
|
configurable: true,
|
|
141
151
|
writable: true,
|
|
142
|
-
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()
|
|
143
166
|
});
|
|
144
167
|
// Telemetry configuration (resolved from options and env vars, see setupTelemetry)
|
|
145
168
|
Object.defineProperty(this, "telemetryEnabled", {
|
|
@@ -154,6 +177,19 @@ export class ActorsMcpServer {
|
|
|
154
177
|
writable: true,
|
|
155
178
|
value: void 0
|
|
156
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
|
+
});
|
|
157
193
|
// List of widgets that are ready to be served
|
|
158
194
|
Object.defineProperty(this, "availableWidgets", {
|
|
159
195
|
enumerable: true,
|
|
@@ -161,6 +197,16 @@ export class ActorsMcpServer {
|
|
|
161
197
|
writable: true,
|
|
162
198
|
value: new Map()
|
|
163
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
|
+
});
|
|
164
210
|
/** Set in the initialize handler once client capabilities are known. */
|
|
165
211
|
Object.defineProperty(this, "clientSupportsUi", {
|
|
166
212
|
enumerable: true,
|
|
@@ -168,18 +214,15 @@ export class ActorsMcpServer {
|
|
|
168
214
|
writable: true,
|
|
169
215
|
value: false
|
|
170
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
|
+
});
|
|
171
224
|
this.options = options;
|
|
172
|
-
this.
|
|
173
|
-
// for stdio use in memory task store if not provided, otherwise use provided task store
|
|
174
|
-
if (this.options.transportType === 'stdio' && !this.options.taskStore) {
|
|
175
|
-
this.taskStore = new InMemoryTaskStore();
|
|
176
|
-
}
|
|
177
|
-
else if (this.options.taskStore) {
|
|
178
|
-
this.taskStore = this.options.taskStore;
|
|
179
|
-
}
|
|
180
|
-
else {
|
|
181
|
-
throw new Error('Task store must be provided for non-stdio transport types');
|
|
182
|
-
}
|
|
225
|
+
this._clientContext = buildMcpClientContext((_a = options.initializeRequestData) === null || _a === void 0 ? void 0 : _a.params);
|
|
183
226
|
this.actorStore = options.actorStore;
|
|
184
227
|
// Constructor is an ingestion boundary for programmatic callers. Normalize via
|
|
185
228
|
// parseServerMode so that runtime-invalid values ('openai' alias, stray strings)
|
|
@@ -192,44 +235,19 @@ export class ActorsMcpServer {
|
|
|
192
235
|
rawServerMode !== undefined ? parseServerMode(rawServerMode) : parseServerMode(legacyUiMode);
|
|
193
236
|
// Preliminary resolution — re-resolved inside the initialize handler once
|
|
194
237
|
// client capabilities are known (only for 'auto').
|
|
195
|
-
this.
|
|
238
|
+
this._serverMode = resolveServerMode(this.serverModeOption, false);
|
|
196
239
|
this.serverModeResolved = this.serverModeOption !== 'auto';
|
|
197
|
-
const { setupSigintHandler = true } = options;
|
|
198
|
-
this.server = new Server(getServerInfo(), {
|
|
199
|
-
capabilities: {
|
|
200
|
-
tools: {
|
|
201
|
-
listChanged: true,
|
|
202
|
-
},
|
|
203
|
-
// Declare long-running task support
|
|
204
|
-
tasks: {
|
|
205
|
-
list: {},
|
|
206
|
-
cancel: {},
|
|
207
|
-
requests: {
|
|
208
|
-
tools: {
|
|
209
|
-
call: {},
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
},
|
|
213
|
-
// Declared but unused — some clients (e.g. Claude Desktop) fail without it.
|
|
214
|
-
resources: {},
|
|
215
|
-
prompts: {},
|
|
216
|
-
logging: {},
|
|
217
|
-
},
|
|
218
|
-
instructions: getServerInstructions(),
|
|
219
|
-
});
|
|
220
240
|
const { telemetryEnabled, telemetryEnv } = this.setupTelemetry();
|
|
221
241
|
this.telemetryEnabled = telemetryEnabled;
|
|
222
242
|
this.telemetryEnv = telemetryEnv;
|
|
223
|
-
this.setupInitializeHandler();
|
|
224
|
-
this.setupLoggingProxy();
|
|
225
243
|
this.tools = new Map();
|
|
226
|
-
this.
|
|
227
|
-
this.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
this.
|
|
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);
|
|
233
251
|
}
|
|
234
252
|
/**
|
|
235
253
|
* Telemetry configuration with precedence: explicit options > env vars > defaults
|
|
@@ -253,51 +271,83 @@ export class ActorsMcpServer {
|
|
|
253
271
|
return { telemetryEnabled, telemetryEnv };
|
|
254
272
|
}
|
|
255
273
|
/**
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
* fire-and-forget (separate microtask), so a follow-up `tools/list` can race past them.
|
|
262
|
-
* 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.
|
|
263
279
|
*/
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const sdkInitHandler = this.server._oninitialize.bind(this.server);
|
|
274
|
-
this.server.setRequestHandler(InitializeRequestSchema, async (request) => {
|
|
275
|
-
var _a;
|
|
276
|
-
this.clientContext = buildMcpClientContext(request.params);
|
|
277
|
-
this.options.initializeRequestData = request;
|
|
278
|
-
this.clientSupportsUi = isUiSupportedByClient(this.clientContext);
|
|
279
|
-
if (this.serverModeOption === 'auto') {
|
|
280
|
-
const resolved = resolveServerMode('auto', this.clientSupportsUi);
|
|
281
|
-
if (resolved !== this.serverMode) {
|
|
282
|
-
this.serverMode = resolved;
|
|
283
|
-
}
|
|
284
|
-
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;
|
|
285
289
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
await this.resolveWidgets();
|
|
294
|
-
const result = await sdkInitHandler(request);
|
|
295
|
-
// Tools are final here (composePendingToolsForClient ran above, applying the per-client
|
|
296
|
-
// blocklist), so tool presence is the ground truth for whether to advertise
|
|
297
|
-
// report-problem in the instructions.
|
|
298
|
-
result.instructions = getServerInstructions(this.serverMode, this.tools.has(HELPER_TOOLS.PROBLEM_REPORT));
|
|
299
|
-
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,
|
|
300
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
|
+
};
|
|
301
351
|
}
|
|
302
352
|
/** True once the connecting client is known (set in the initialize handler, or hydrated by a
|
|
303
353
|
* recovery path). Only client-gated tools wait for this so the per-client blocklist can be
|
|
@@ -306,37 +356,34 @@ export class ActorsMcpServer {
|
|
|
306
356
|
return this.clientContext != null;
|
|
307
357
|
}
|
|
308
358
|
/**
|
|
309
|
-
* Compose
|
|
310
|
-
* report-problem unless
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
* without an initialize still restores them. report-problem is withheld until the client is known
|
|
314
|
-
* and re-added by the initialize flush. Used by every input-driven load path and the flush.
|
|
315
|
-
* (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.
|
|
316
363
|
*/
|
|
317
|
-
composeToolsForClient(
|
|
318
|
-
const tools = getToolsForServerMode(input, actorTools,
|
|
319
|
-
if (this.isReportProblemServable())
|
|
364
|
+
composeToolsForClient(source, view) {
|
|
365
|
+
const tools = getToolsForServerMode(source.input, source.actorTools, view.serverMode);
|
|
366
|
+
if (this.isReportProblemServable(view))
|
|
320
367
|
return tools;
|
|
321
368
|
return tools.filter((tool) => tool.name !== HELPER_TOOLS.PROBLEM_REPORT);
|
|
322
369
|
}
|
|
323
370
|
/**
|
|
324
|
-
* Whether report-problem may be served
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
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.
|
|
331
378
|
*/
|
|
332
|
-
isReportProblemServable() {
|
|
333
|
-
return this.telemetryEnabled &&
|
|
379
|
+
isReportProblemServable(view) {
|
|
380
|
+
return (this.telemetryEnabled && view.clientContext != null && !isReportProblemBlockedForClient(view.clientContext));
|
|
334
381
|
}
|
|
335
382
|
composePendingToolsForClient() {
|
|
336
|
-
if (this.pendingToolsUntilClientKnown.
|
|
383
|
+
if (this.pendingToolsUntilClientKnown.size === 0)
|
|
337
384
|
return;
|
|
338
|
-
const tools = this.pendingToolsUntilClientKnown.flatMap((
|
|
339
|
-
this.pendingToolsUntilClientKnown
|
|
385
|
+
const tools = [...this.pendingToolsUntilClientKnown.values()].flatMap((source) => this.composeToolsForClient(source, this.servingContext));
|
|
386
|
+
this.pendingToolsUntilClientKnown.clear();
|
|
340
387
|
// Load paths already upserted the client-agnostic tools pre-init; re-upserting is
|
|
341
388
|
// idempotent, and this pass adds the client-gated tools (e.g. report-problem) now that the
|
|
342
389
|
// client is known.
|
|
@@ -382,25 +429,26 @@ export class ActorsMcpServer {
|
|
|
382
429
|
return [...this.listInternalToolNames(), ...this.listActorToolNames(), ...this.listActorMcpServerToolIds()];
|
|
383
430
|
}
|
|
384
431
|
/**
|
|
385
|
-
* Buffer-or-compose gate shared by the actor-tools loaders.
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
* withholds report-problem until the client is known) and upsert it; if the client still isn't
|
|
390
|
-
* 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.
|
|
391
436
|
*/
|
|
392
437
|
registerFetchedActorTools(input, actorTools) {
|
|
438
|
+
const source = { input, actorTools };
|
|
439
|
+
const key = toolSourceKey(input);
|
|
440
|
+
this.toolSources.set(key, source);
|
|
393
441
|
if (!this.serverModeResolved) {
|
|
394
|
-
this.pendingToolsUntilClientKnown.
|
|
442
|
+
this.pendingToolsUntilClientKnown.set(key, source);
|
|
395
443
|
if (actorTools.length > 0)
|
|
396
444
|
this.upsertTools(actorTools);
|
|
397
445
|
return;
|
|
398
446
|
}
|
|
399
|
-
const tools = this.composeToolsForClient(
|
|
447
|
+
const tools = this.composeToolsForClient(source, this.servingContext);
|
|
400
448
|
if (tools.length > 0)
|
|
401
449
|
this.upsertTools(tools);
|
|
402
450
|
if (!this.clientKnown)
|
|
403
|
-
this.pendingToolsUntilClientKnown.
|
|
451
|
+
this.pendingToolsUntilClientKnown.set(key, source);
|
|
404
452
|
}
|
|
405
453
|
/**
|
|
406
454
|
* Loads missing toolNames from a provided list of tool names.
|
|
@@ -429,14 +477,9 @@ export class ActorsMcpServer {
|
|
|
429
477
|
this.registerFetchedActorTools(input, actorTools);
|
|
430
478
|
}
|
|
431
479
|
/**
|
|
432
|
-
* Two-phase: getActors (async, client-agnostic
|
|
433
|
-
*
|
|
434
|
-
* the
|
|
435
|
-
* tools are withheld until the client is known, and the source is queued so the flush adds them.
|
|
436
|
-
*
|
|
437
|
-
* Don't move the getActors await into the initialize handler — clients time out
|
|
438
|
-
* waiting for InitializeResult. The queue buffers already-fetched data, not network
|
|
439
|
-
* 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.
|
|
440
483
|
*/
|
|
441
484
|
async loadToolsFromInput(input, apifyClient) {
|
|
442
485
|
const actorTools = await getActors(input, apifyClient, {
|
|
@@ -445,86 +488,22 @@ export class ActorsMcpServer {
|
|
|
445
488
|
});
|
|
446
489
|
this.registerFetchedActorTools(input, actorTools);
|
|
447
490
|
}
|
|
448
|
-
/** Delete tools from the server. */
|
|
449
|
-
removeToolsByName(toolNames) {
|
|
450
|
-
const removedTools = [];
|
|
451
|
-
for (const toolName of toolNames) {
|
|
452
|
-
if (this.removeToolByName(toolName)) {
|
|
453
|
-
removedTools.push(toolName);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
return removedTools;
|
|
457
|
-
}
|
|
458
491
|
/**
|
|
459
|
-
* Upsert new tools.
|
|
460
|
-
*
|
|
461
|
-
*
|
|
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.
|
|
462
495
|
*/
|
|
463
496
|
upsertTools(tools) {
|
|
464
|
-
// Client gating
|
|
465
|
-
//
|
|
466
|
-
// 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.
|
|
467
499
|
for (const tool of tools) {
|
|
468
|
-
const stored = this.
|
|
500
|
+
const stored = this.toStoredTool(tool);
|
|
469
501
|
this.tools.set(stored.name, stored);
|
|
470
502
|
}
|
|
471
503
|
return tools;
|
|
472
504
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
this.tools.delete(toolName);
|
|
476
|
-
log.debug('Deleted tool', { toolName });
|
|
477
|
-
return true;
|
|
478
|
-
}
|
|
479
|
-
return false;
|
|
480
|
-
}
|
|
481
|
-
setupErrorHandling(setupSIGINTHandler = true) {
|
|
482
|
-
this.server.onerror = (error) => {
|
|
483
|
-
var _a;
|
|
484
|
-
// Known client faults are expected noise, not server bugs — softFail so they don't
|
|
485
|
-
// flood Mezmo error alerts. The fault patterns live in utils/logging.ts.
|
|
486
|
-
const message = (_a = error.message) !== null && _a !== void 0 ? _a : '';
|
|
487
|
-
if (isMcpClientFaultMessage(message)) {
|
|
488
|
-
// Sanitize the errMessage value to preserve the soft-fail level (Mezmo promotes
|
|
489
|
-
// entries whose message contains "error").
|
|
490
|
-
log.softFail('MCP client fault, request could not be handled', {
|
|
491
|
-
errMessage: sanitizeMezmoMessage(message),
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
else {
|
|
495
|
-
log.error('[MCP Error]', { error });
|
|
496
|
-
}
|
|
497
|
-
};
|
|
498
|
-
if (setupSIGINTHandler) {
|
|
499
|
-
const handler = async () => {
|
|
500
|
-
await this.server.close();
|
|
501
|
-
process.exit(0);
|
|
502
|
-
};
|
|
503
|
-
process.once('SIGINT', handler);
|
|
504
|
-
this.sigintHandler = handler;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
setupLoggingProxy() {
|
|
508
|
-
const originalSendLoggingMessage = this.server.sendLoggingMessage.bind(this.server);
|
|
509
|
-
// Filter outgoing log messages below the client's requested level.
|
|
510
|
-
this.server.sendLoggingMessage = async (params) => {
|
|
511
|
-
var _a, _b;
|
|
512
|
-
const messageLevelValue = (_a = LOG_LEVEL_MAP[params.level]) !== null && _a !== void 0 ? _a : -1; // Unknown levels get -1, discard
|
|
513
|
-
const currentLevelValue = (_b = LOG_LEVEL_MAP[this.currentLogLevel]) !== null && _b !== void 0 ? _b : LOG_LEVEL_MAP.info; // Default to info if invalid
|
|
514
|
-
if (messageLevelValue >= currentLevelValue) {
|
|
515
|
-
await originalSendLoggingMessage(params);
|
|
516
|
-
}
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
setupLoggingHandlers() {
|
|
520
|
-
this.server.setRequestHandler(SetLevelRequestSchema, (request) => {
|
|
521
|
-
const { level } = request.params;
|
|
522
|
-
if (LOG_LEVEL_MAP[level] !== undefined) {
|
|
523
|
-
this.currentLogLevel = level;
|
|
524
|
-
}
|
|
525
|
-
// Sending empty result based on MCP spec
|
|
526
|
-
return {};
|
|
527
|
-
});
|
|
505
|
+
toStoredTool(tool) {
|
|
506
|
+
return this.options.paymentProvider ? this.options.paymentProvider.decorateToolSchema(tool) : tool;
|
|
528
507
|
}
|
|
529
508
|
/**
|
|
530
509
|
* Token sources in order: per-request `_meta.apifyToken` (stdio inline) > server-instance
|
|
@@ -542,428 +521,54 @@ export class ActorsMcpServer {
|
|
|
542
521
|
* Still carries the request-origin tag from the client context captured by this point.
|
|
543
522
|
*/
|
|
544
523
|
resolveApifyClient(params) {
|
|
545
|
-
|
|
546
|
-
return token
|
|
547
|
-
? new ApifyClient({ token, requestOrigin: getRequestOriginForClient(this.clientContext) })
|
|
548
|
-
: undefined;
|
|
524
|
+
return this.createApifyClient(this.resolveApifyToken(params._meta), this.clientContext);
|
|
549
525
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
getMode: () => this.serverMode,
|
|
554
|
-
getAvailableWidgets: () => this.availableWidgets,
|
|
555
|
-
});
|
|
556
|
-
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
557
|
-
return await resourceService.listResources();
|
|
558
|
-
});
|
|
559
|
-
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
560
|
-
try {
|
|
561
|
-
return await resourceService.readResource(request.params.uri, this.resolveApifyClient(request.params));
|
|
562
|
-
}
|
|
563
|
-
catch (error) {
|
|
564
|
-
throw toLegacyMcpError(error);
|
|
565
|
-
}
|
|
566
|
-
});
|
|
567
|
-
this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
568
|
-
return await resourceService.listResourceTemplates();
|
|
569
|
-
});
|
|
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;
|
|
570
529
|
}
|
|
571
530
|
/**
|
|
572
|
-
*
|
|
573
|
-
*
|
|
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.
|
|
574
535
|
*/
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
try {
|
|
580
|
-
return promptService.getPrompt(request.params.name, request.params.arguments);
|
|
581
|
-
}
|
|
582
|
-
catch (error) {
|
|
583
|
-
throw toLegacyMcpError(error);
|
|
584
|
-
}
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
/**
|
|
588
|
-
* Fetches a task by ID, softFail-logging and throwing a client-facing McpError if it doesn't exist.
|
|
589
|
-
*/
|
|
590
|
-
async getTaskOrThrow(taskId, mcpSessionId, logTag) {
|
|
591
|
-
const task = await this.taskStore.getTask(taskId, mcpSessionId);
|
|
592
|
-
if (!task) {
|
|
593
|
-
// Client error (invalid/unknown taskId) — softFail to avoid polluting error logs.
|
|
594
|
-
log.softFail(`[${logTag}] Task not found`, { taskId, mcpSessionId, statusCode: 404 });
|
|
595
|
-
throw new McpError(ErrorCode.InvalidParams, `Task "${taskId}" not found`);
|
|
596
|
-
}
|
|
597
|
-
return task;
|
|
598
|
-
}
|
|
599
|
-
/**
|
|
600
|
-
* Sets up MCP request handlers for long-running tasks.
|
|
601
|
-
* Each handler reads `_meta.mcpSessionId` (injected at the transport layer) to isolate
|
|
602
|
-
* per-session task stores.
|
|
603
|
-
*/
|
|
604
|
-
setupTaskHandlers() {
|
|
605
|
-
// List tasks
|
|
606
|
-
this.server.setRequestHandler(ListTasksRequestSchema, async (request) => {
|
|
607
|
-
var _a;
|
|
608
|
-
const params = (request.params || {});
|
|
609
|
-
const { cursor } = params;
|
|
610
|
-
const mcpSessionId = (_a = params._meta) === null || _a === void 0 ? void 0 : _a.mcpSessionId;
|
|
611
|
-
log.debug('[ListTasksRequestSchema] Listing tasks', { mcpSessionId });
|
|
612
|
-
const result = await this.taskStore.listTasks(cursor, mcpSessionId);
|
|
613
|
-
return { tasks: result.tasks, nextCursor: result.nextCursor };
|
|
614
|
-
});
|
|
615
|
-
// Get task status
|
|
616
|
-
this.server.setRequestHandler(GetTaskRequestSchema, async (request) => {
|
|
617
|
-
var _a;
|
|
618
|
-
const params = (request.params || {});
|
|
619
|
-
const { taskId } = params;
|
|
620
|
-
const mcpSessionId = (_a = params._meta) === null || _a === void 0 ? void 0 : _a.mcpSessionId;
|
|
621
|
-
log.debug('[GetTaskRequestSchema] Getting task status', { taskId, mcpSessionId });
|
|
622
|
-
return await this.getTaskOrThrow(taskId, mcpSessionId, 'GetTaskRequestSchema');
|
|
623
|
-
});
|
|
624
|
-
// Get task result payload
|
|
625
|
-
this.server.setRequestHandler(GetTaskPayloadRequestSchema, async (request) => {
|
|
626
|
-
var _a;
|
|
627
|
-
const params = (request.params || {});
|
|
628
|
-
const { taskId } = params;
|
|
629
|
-
const mcpSessionId = (_a = params._meta) === null || _a === void 0 ? void 0 : _a.mcpSessionId;
|
|
630
|
-
log.debug('[GetTaskPayloadRequestSchema] Getting task result', { taskId, mcpSessionId });
|
|
631
|
-
const task = await this.getTaskOrThrow(taskId, mcpSessionId, 'GetTaskPayloadRequestSchema');
|
|
632
|
-
if (task.status !== 'completed' && task.status !== 'failed') {
|
|
633
|
-
throw new McpError(ErrorCode.InvalidParams, `Task "${taskId}" is not completed yet. Current status: ${task.status}`);
|
|
634
|
-
}
|
|
635
|
-
const result = await this.taskStore.getTaskResult(taskId, mcpSessionId);
|
|
636
|
-
// taskId is not in the result body — _meta.related-task lets clients correlate them
|
|
637
|
-
return {
|
|
638
|
-
...result,
|
|
639
|
-
_meta: {
|
|
640
|
-
...result._meta,
|
|
641
|
-
[RELATED_TASK_META_KEY]: { taskId },
|
|
642
|
-
},
|
|
643
|
-
};
|
|
644
|
-
});
|
|
645
|
-
// Cancel task
|
|
646
|
-
this.server.setRequestHandler(CancelTaskRequestSchema, async (request) => {
|
|
647
|
-
var _a;
|
|
648
|
-
const params = (request.params || {});
|
|
649
|
-
const { taskId } = params;
|
|
650
|
-
const mcpSessionId = (_a = params._meta) === null || _a === void 0 ? void 0 : _a.mcpSessionId;
|
|
651
|
-
log.debug('[CancelTaskRequestSchema] Cancelling task', { taskId, mcpSessionId });
|
|
652
|
-
const task = await this.getTaskOrThrow(taskId, mcpSessionId, 'CancelTaskRequestSchema');
|
|
653
|
-
if (task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled') {
|
|
654
|
-
// Client error (cancel on terminal task) — softFail to avoid polluting error logs.
|
|
655
|
-
log.softFail('[CancelTaskRequestSchema] Task already in terminal state', {
|
|
656
|
-
taskId,
|
|
657
|
-
mcpSessionId,
|
|
658
|
-
status: task.status,
|
|
659
|
-
statusCode: 409,
|
|
660
|
-
});
|
|
661
|
-
throw new McpError(ErrorCode.InvalidParams, `Cannot cancel task "${taskId}" with status "${task.status}"`);
|
|
662
|
-
}
|
|
663
|
-
await this.taskStore.updateTaskStatus(taskId, 'cancelled', 'Cancelled by client', mcpSessionId);
|
|
664
|
-
const updatedTask = await this.taskStore.getTask(taskId, mcpSessionId);
|
|
665
|
-
log.debug('[CancelTaskRequestSchema] Task cancelled successfully', { taskId, mcpSessionId });
|
|
666
|
-
await emitTaskStatusNotification(taskId, mcpSessionId, this.taskStore, this.server);
|
|
667
|
-
return updatedTask;
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
setupToolHandlers() {
|
|
671
|
-
// Handles the request to list tools.
|
|
672
|
-
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
673
|
-
const tools = Array.from(this.tools.values()).map((tool) => getToolPublicFieldOnly(tool, {
|
|
674
|
-
mode: this.serverMode,
|
|
675
|
-
filterWidgetMeta: true,
|
|
676
|
-
}));
|
|
677
|
-
return { tools };
|
|
678
|
-
});
|
|
679
|
-
/**
|
|
680
|
-
* Handles the request to call a tool. `extra` carries request-scoped helpers such as
|
|
681
|
-
* `sendNotification`. Throws {@link McpError} to mirror the MCP SDK's McpServer error codes.
|
|
682
|
-
*/
|
|
683
|
-
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
684
|
-
var _a, _b, _c;
|
|
685
|
-
const params = request.params;
|
|
686
|
-
// Keep telemetry on the decoded arguments.
|
|
687
|
-
// eslint-disable-next-line prefer-const
|
|
688
|
-
let { name, arguments: args, _meta: meta } = params;
|
|
689
|
-
const progressToken = meta === null || meta === void 0 ? void 0 : meta.progressToken;
|
|
690
|
-
const apifyToken = this.resolveApifyToken(meta);
|
|
691
|
-
// Injected upstream; required for long-running tasks — the task store keys on it and
|
|
692
|
-
// there is no other channel to pass it.
|
|
693
|
-
const mcpSessionId = meta === null || meta === void 0 ? void 0 : meta.mcpSessionId;
|
|
694
|
-
if (!mcpSessionId) {
|
|
695
|
-
log.error('MCP Session ID is missing in tool call request. This should never happen.');
|
|
696
|
-
throw new Error('MCP Session ID is required for tool calls');
|
|
697
|
-
}
|
|
698
|
-
const startTime = Date.now();
|
|
699
|
-
let toolStatus = TOOL_STATUS.SUCCEEDED;
|
|
700
|
-
let callDiagnostics = {};
|
|
701
|
-
let shouldTrackTelemetry = true;
|
|
702
|
-
let resolvedToolName = name;
|
|
703
|
-
// Set only on the pre-flight task path — the one task-mode flow whose telemetry rides
|
|
704
|
-
// this handler's `finally` — so its `Tool call completed` log line keeps the taskId the
|
|
705
|
-
// async path logs via finishTaskTracking.
|
|
706
|
-
let preflightTaskId;
|
|
707
|
-
// The nudge must be included in the measured result.
|
|
708
|
-
let toolResult = null;
|
|
709
|
-
// Keep actor context available to the outer catch.
|
|
710
|
-
let actorName;
|
|
711
|
-
let actorId;
|
|
712
|
-
const { clientContext } = this;
|
|
713
|
-
// Start with the raw name so early failures still have telemetry.
|
|
714
|
-
const { telemetryData, userId } = await prepareTelemetryData({
|
|
715
|
-
toolName: name,
|
|
716
|
-
mcpSessionId,
|
|
717
|
-
apifyToken,
|
|
718
|
-
apifyMcpServer: this,
|
|
719
|
-
clientContext,
|
|
720
|
-
});
|
|
721
|
-
try {
|
|
722
|
-
const prepared = await prepareToolCall({
|
|
723
|
-
apifyMcpServer: this,
|
|
724
|
-
apifyToken,
|
|
725
|
-
name,
|
|
726
|
-
args,
|
|
727
|
-
meta,
|
|
728
|
-
requestHeaders: (_a = extra.requestInfo) === null || _a === void 0 ? void 0 : _a.headers,
|
|
729
|
-
isTaskRequest: Boolean(request.params.task),
|
|
730
|
-
mcpSessionId,
|
|
731
|
-
telemetryData,
|
|
732
|
-
clientContext,
|
|
733
|
-
extra,
|
|
734
|
-
});
|
|
735
|
-
if ('result' in prepared) {
|
|
736
|
-
// The engine already classified this post-resolution failure.
|
|
737
|
-
resolvedToolName = prepared.resolvedToolName;
|
|
738
|
-
args = prepared.decodedArgs;
|
|
739
|
-
toolStatus = prepared.toolStatus;
|
|
740
|
-
callDiagnostics = prepared.callDiagnostics;
|
|
741
|
-
toolResult = prepared.result;
|
|
742
|
-
return prepared.result;
|
|
743
|
-
}
|
|
744
|
-
if ('message' in prepared) {
|
|
745
|
-
// Reproduce v1's invalid-params tail and preserve telemetry fields.
|
|
746
|
-
resolvedToolName = (_b = prepared.resolvedToolName) !== null && _b !== void 0 ? _b : resolvedToolName;
|
|
747
|
-
if (prepared.decodedArgs)
|
|
748
|
-
args = prepared.decodedArgs;
|
|
749
|
-
toolStatus = prepared.toolStatus;
|
|
750
|
-
callDiagnostics = prepared.callDiagnostics;
|
|
751
|
-
log.softFail(prepared.message, {
|
|
752
|
-
mcpSessionId,
|
|
753
|
-
failureCategory: prepared.callDiagnostics.failure_category,
|
|
754
|
-
actorName: prepared.callDiagnostics.actor_name,
|
|
755
|
-
validationKeyword: prepared.callDiagnostics.validation_keyword,
|
|
756
|
-
validationPath: prepared.callDiagnostics.validation_path,
|
|
757
|
-
validationMissingProperty: prepared.callDiagnostics.validation_missing_property,
|
|
758
|
-
validationAdditionalProperty: prepared.callDiagnostics.validation_additional_property,
|
|
759
|
-
...prepared.logFields,
|
|
760
|
-
});
|
|
761
|
-
await this.server.sendLoggingMessage({ level: 'error', data: prepared.message });
|
|
762
|
-
throw new McpError(ErrorCode.InvalidParams, prepared.message);
|
|
763
|
-
}
|
|
764
|
-
const { tool, toolArgs, logSafeArgs, apifyClient, standbyRejection, paymentRequiredResult } = prepared;
|
|
765
|
-
actorName = prepared.actorName;
|
|
766
|
-
actorId = prepared.actorId;
|
|
767
|
-
resolvedToolName = getToolFullName(tool);
|
|
768
|
-
// Telemetry uses the decoded arguments.
|
|
769
|
-
args = prepared.decodedArgs;
|
|
770
|
-
// TODO: we should split this huge method into smaller parts as it is slowly getting out of hand
|
|
771
|
-
// Handle long-running task request
|
|
772
|
-
if (request.params.task) {
|
|
773
|
-
const task = await this.taskStore.createTask({
|
|
774
|
-
ttl: request.params.task.ttl,
|
|
775
|
-
}, `call-tool-${name}-${randomUUID()}`, request, mcpSessionId);
|
|
776
|
-
log.debug('Created task for tool execution', {
|
|
777
|
-
taskId: task.taskId,
|
|
778
|
-
toolName: tool.name,
|
|
779
|
-
mcpSessionId,
|
|
780
|
-
});
|
|
781
|
-
// Pre-flight failure is already known — the outcome needs no work. Resolve the
|
|
782
|
-
// task synchronously: store the failure as the terminal `completed` result and emit
|
|
783
|
-
// exactly one `completed` status notification (no `updateTaskStatus('working')`, so no
|
|
784
|
-
// `working` notification). Standby rejection wins over the generic payment-required
|
|
785
|
-
// short-circuit, matching the sync path's precedence. Telemetry rides the handler's
|
|
786
|
-
// outer `finally` (shouldTrackTelemetry stays true), firing once with the sync-path
|
|
787
|
-
// properties plus the taskId on the log line.
|
|
788
|
-
const preflightResult = standbyRejection !== null && standbyRejection !== void 0 ? standbyRejection : paymentRequiredResult;
|
|
789
|
-
if (preflightResult) {
|
|
790
|
-
const outcome = buildPreflightFailureOutcome(standbyRejection, paymentRequiredResult, actorName, actorId);
|
|
791
|
-
toolStatus = outcome.toolStatus;
|
|
792
|
-
callDiagnostics = outcome.callDiagnostics;
|
|
793
|
-
preflightTaskId = task.taskId;
|
|
794
|
-
try {
|
|
795
|
-
await storeTaskResultOrSkipIfExpired(this.taskStore, tool.name, task.taskId, 'completed', outcome.result, mcpSessionId);
|
|
796
|
-
}
|
|
797
|
-
catch (error) {
|
|
798
|
-
// A store failure (not expiry) would otherwise fall through to the generic
|
|
799
|
-
// catch and return a task-less tool result the client rejects as a
|
|
800
|
-
// CreateTaskResult parse error; surface it as a protocol error instead.
|
|
801
|
-
// The pre-flight outcome left toolStatus=SOFT_FAIL, but a genuine store
|
|
802
|
-
// outage is a hard failure — correct it before throwing so the handler
|
|
803
|
-
// `finally` logs FAILED/INTERNAL_ERROR, not the stale pre-flight SOFT_FAIL.
|
|
804
|
-
toolStatus = TOOL_STATUS.FAILED;
|
|
805
|
-
callDiagnostics = {
|
|
806
|
-
failure_category: FAILURE_CATEGORY.INTERNAL_ERROR,
|
|
807
|
-
...buildActorFields(actorName, actorId),
|
|
808
|
-
};
|
|
809
|
-
throw new McpError(ErrorCode.InternalError, `Failed to store the pre-flight result for task "${task.taskId}": ${error instanceof Error ? error.message : String(error)}`);
|
|
810
|
-
}
|
|
811
|
-
// Defer so the client sees the CreateTaskResult (and learns the taskId) before
|
|
812
|
-
// the terminal status notification — the async path's post-response ordering.
|
|
813
|
-
// emitTaskStatusNotification never throws and no-ops if the task expired.
|
|
814
|
-
setImmediate(() => {
|
|
815
|
-
void emitTaskStatusNotification(task.taskId, mcpSessionId, this.taskStore, this.server);
|
|
816
|
-
});
|
|
817
|
-
// Measure the nudged result without changing the stored result.
|
|
818
|
-
toolResult = withReportProblemNudge({
|
|
819
|
-
result: outcome.result,
|
|
820
|
-
tools: this.tools,
|
|
821
|
-
failingToolName: resolvedToolName,
|
|
822
|
-
failureCategory: callDiagnostics.failure_category,
|
|
823
|
-
failureHttpStatus: callDiagnostics.failure_http_status,
|
|
824
|
-
});
|
|
825
|
-
// createTask returned status `working`; synthesize the terminal status instead of
|
|
826
|
-
// re-fetching — if the task expired before the result store (the one case
|
|
827
|
-
// storeTaskResultOrSkipIfExpired tolerates), a re-fetch would come back empty and a
|
|
828
|
-
// `working` fallback would contradict the tasks/get 404 the client sees next.
|
|
829
|
-
return { task: { ...task, status: 'completed' } };
|
|
830
|
-
}
|
|
831
|
-
// Execute the tool asynchronously and update task status
|
|
832
|
-
setImmediate(() => {
|
|
833
|
-
executeToolAndUpdateTask({
|
|
834
|
-
taskId: task.taskId,
|
|
835
|
-
tool,
|
|
836
|
-
toolArgs,
|
|
837
|
-
logSafeArgs,
|
|
838
|
-
apifyClient,
|
|
839
|
-
apifyToken,
|
|
840
|
-
progressToken,
|
|
841
|
-
extra,
|
|
842
|
-
mcpSessionId,
|
|
843
|
-
actorName,
|
|
844
|
-
actorId,
|
|
845
|
-
apifyMcpServer: this,
|
|
846
|
-
clientContext,
|
|
847
|
-
}).catch((error) =>
|
|
848
|
-
// Benign task-expiry is handled in-method (see the catch block and
|
|
849
|
-
// storeTaskResultOrSkipIfExpired); anything reaching here is genuinely unexpected.
|
|
850
|
-
log.error('executeToolAndUpdateTask failed unexpectedly', { taskId: task.taskId, error }));
|
|
851
|
-
});
|
|
852
|
-
// Return the task immediately; execution continues asynchronously
|
|
853
|
-
shouldTrackTelemetry = false;
|
|
854
|
-
return { task };
|
|
855
|
-
}
|
|
856
|
-
// Sync path: run the shared dispatch tail and project its neutral outcome by identity.
|
|
857
|
-
const outcome = await executeSyncToolCall(prepared, {
|
|
858
|
-
apifyMcpServer: this,
|
|
859
|
-
apifyToken,
|
|
860
|
-
toolName: name,
|
|
861
|
-
mcpSessionId,
|
|
862
|
-
progressToken,
|
|
863
|
-
extra,
|
|
864
|
-
});
|
|
865
|
-
toolStatus = outcome.toolStatus;
|
|
866
|
-
callDiagnostics = outcome.callDiagnostics;
|
|
867
|
-
toolResult = outcome.result;
|
|
868
|
-
return outcome.result;
|
|
869
|
-
}
|
|
870
|
-
catch (error) {
|
|
871
|
-
// Match v1: classify task-creation failures, but re-throw protocol errors as JSON-RPC.
|
|
872
|
-
if (error instanceof McpError) {
|
|
873
|
-
throw error;
|
|
874
|
-
}
|
|
875
|
-
const outcome = classifyToolCallError(error, {
|
|
876
|
-
apifyMcpServer: this,
|
|
877
|
-
toolName: name,
|
|
878
|
-
failingToolName: resolvedToolName,
|
|
879
|
-
actorName,
|
|
880
|
-
actorId,
|
|
881
|
-
isAborted: Boolean((_c = extra.signal) === null || _c === void 0 ? void 0 : _c.aborted),
|
|
882
|
-
mcpSessionId,
|
|
883
|
-
});
|
|
884
|
-
toolStatus = outcome.toolStatus;
|
|
885
|
-
callDiagnostics = outcome.callDiagnostics;
|
|
886
|
-
toolResult = outcome.result;
|
|
887
|
-
return outcome.result;
|
|
888
|
-
}
|
|
889
|
-
finally {
|
|
890
|
-
if (shouldTrackTelemetry) {
|
|
891
|
-
logToolCallAndTelemetry({
|
|
892
|
-
toolName: resolvedToolName,
|
|
893
|
-
mcpSessionId,
|
|
894
|
-
toolStatus,
|
|
895
|
-
startTime,
|
|
896
|
-
telemetryData,
|
|
897
|
-
userId,
|
|
898
|
-
callDiagnostics,
|
|
899
|
-
args,
|
|
900
|
-
result: toolResult,
|
|
901
|
-
taskId: preflightTaskId,
|
|
902
|
-
apifyMcpServer: this,
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
});
|
|
907
|
-
}
|
|
908
|
-
/**
|
|
909
|
-
* Resolves widgets and determines which ones are ready to be served.
|
|
910
|
-
*/
|
|
911
|
-
async resolveWidgets() {
|
|
912
|
-
if (this.serverMode !== SERVER_MODE.APPS) {
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
try {
|
|
916
|
-
const { fileURLToPath } = await import('node:url');
|
|
917
|
-
const path = await import('node:path');
|
|
918
|
-
const filename = fileURLToPath(import.meta.url);
|
|
919
|
-
const dirName = path.dirname(filename);
|
|
920
|
-
const resolved = await resolveAvailableWidgets(dirName);
|
|
921
|
-
this.availableWidgets = resolved;
|
|
922
|
-
const readyWidgets = [];
|
|
923
|
-
const missingWidgets = [];
|
|
924
|
-
for (const [uri, widget] of resolved.entries()) {
|
|
925
|
-
if (widget.exists) {
|
|
926
|
-
readyWidgets.push(widget.name);
|
|
927
|
-
}
|
|
928
|
-
else {
|
|
929
|
-
missingWidgets.push(widget.name);
|
|
930
|
-
log.softFail(`Widget file not found: ${widget.jsPath} (widget: ${uri})`);
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
if (readyWidgets.length > 0) {
|
|
934
|
-
log.debug('Ready widgets', { widgets: readyWidgets });
|
|
935
|
-
}
|
|
936
|
-
if (missingWidgets.length > 0) {
|
|
937
|
-
log.softFail('Some widgets are not ready', {
|
|
938
|
-
widgets: missingWidgets,
|
|
939
|
-
note: 'These widgets will not be available. Ensure web/dist files are built and included in deployment.',
|
|
940
|
-
});
|
|
941
|
-
}
|
|
536
|
+
async resolveWidgetsForMode(mode) {
|
|
537
|
+
var _a;
|
|
538
|
+
if (mode !== SERVER_MODE.APPS) {
|
|
539
|
+
return new Map();
|
|
942
540
|
}
|
|
943
|
-
|
|
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;
|
|
944
545
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
945
546
|
log.softFail(`Failed to resolve widgets: ${errorMessage}`);
|
|
946
547
|
// Continue without widgets
|
|
947
|
-
|
|
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);
|
|
948
558
|
}
|
|
949
559
|
async connect(transport) {
|
|
950
|
-
await this.
|
|
951
|
-
await this.
|
|
560
|
+
await this.resolveInstanceWidgets();
|
|
561
|
+
await this.legacyServer.connect(transport);
|
|
952
562
|
}
|
|
953
563
|
async close() {
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
for (const tool of this.tools.values()) {
|
|
960
|
-
if (tool.ajvValidate && typeof tool.ajvValidate === 'function') {
|
|
961
|
-
tool.ajvValidate = null;
|
|
962
|
-
}
|
|
963
|
-
}
|
|
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();
|
|
964
569
|
this.tools.clear();
|
|
965
|
-
|
|
966
|
-
|
|
570
|
+
this.toolSources.clear();
|
|
571
|
+
this.pendingToolsUntilClientKnown.clear();
|
|
967
572
|
}
|
|
968
573
|
}
|
|
969
574
|
//# sourceMappingURL=server.js.map
|