Skip to content

Commit ce90671

Browse files
committed
wip: skyfire pay id token integation, add progress notif integration test
1 parent 586ff45 commit ce90671

27 files changed

+264
-152
lines changed

src/actor/server.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import express from 'express';
1212
import log from '@apify/log';
1313

1414
import { ActorsMcpServer } from '../mcp/server.js';
15+
import type { AuthToken } from '../types.js';
1516
import { getHelpMessage, HEADER_READINESS_PROBE, Routes, TransportType } from './const.js';
1617
import { getActorRunData } from './utils.js';
1718

@@ -73,9 +74,12 @@ export function createExpressApp(
7374
const transport = new SSEServerTransport(Routes.MESSAGE, res);
7475

7576
// Load MCP server tools
76-
const apifyToken = process.env.APIFY_TOKEN as string;
77+
const authToken: AuthToken = {
78+
value: process.env.APIFY_TOKEN as string,
79+
type: 'apify',
80+
};
7781
log.debug('Loading tools from URL', { sessionId: transport.sessionId, tr: TransportType.SSE });
78-
await mcpServer.loadToolsFromUrl(req.url, apifyToken);
82+
await mcpServer.loadToolsFromUrl(req.url, authToken);
7983

8084
transportsSSE[transport.sessionId] = transport;
8185
mcpServers[transport.sessionId] = mcpServer;
@@ -155,9 +159,12 @@ export function createExpressApp(
155159
const mcpServer = new ActorsMcpServer(false);
156160

157161
// Load MCP server tools
158-
const apifyToken = process.env.APIFY_TOKEN as string;
162+
const authToken: AuthToken = {
163+
value: process.env.APIFY_TOKEN as string,
164+
type: 'apify',
165+
};
159166
log.debug('Loading tools from URL', { sessionId: transport.sessionId, tr: TransportType.HTTP });
160-
await mcpServer.loadToolsFromUrl(req.url, apifyToken);
167+
await mcpServer.loadToolsFromUrl(req.url, authToken);
161168

162169
// Connect the transport to the MCP server BEFORE handling the request
163170
await mcpServer.connect(transport);

src/apify-client.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ApifyClient as _ApifyClient } from 'apify-client';
33
import type { AxiosRequestConfig } from 'axios';
44

55
import { USER_AGENT_ORIGIN } from './const.js';
6+
import type { AuthToken } from './types.js';
67

78
/**
89
* Adds a User-Agent header to the request config.
@@ -22,23 +23,59 @@ export function getApifyAPIBaseUrl(): string {
2223
return process.env.APIFY_API_BASE_URL || 'https://api.apify.com';
2324
}
2425

26+
/**
27+
* Adds Skyfire header to the request config if needed.
28+
* @param config
29+
* @param authToken
30+
* @private
31+
*/
32+
function addSkyfireHeader(config: AxiosRequestConfig, authToken?: AuthToken): AxiosRequestConfig {
33+
if (authToken?.type === 'skyfire') {
34+
const updatedConfig = { ...config };
35+
updatedConfig.headers = updatedConfig.headers ?? {};
36+
updatedConfig.headers['skyfire-pay-id'] = authToken.value;
37+
return updatedConfig;
38+
}
39+
return config;
40+
}
41+
2542
export class ApifyClient extends _ApifyClient {
26-
constructor(options: ApifyClientOptions) {
43+
constructor(options: ApifyClientOptions & { authToken?: AuthToken }) {
44+
// Destructure to separate authToken from other options
45+
const { authToken, ...clientOptions } = options;
46+
2747
/**
2848
* In order to publish to DockerHub, we need to run their build task to validate our MCP server.
2949
* This was failing since we were sending this dummy token to Apify in order to build the Actor tools.
3050
* So if we encounter this dummy value, we remove it to use Apify client as unauthenticated, which is sufficient
3151
* for server start and listing of tools.
3252
*/
33-
if (options.token?.toLowerCase() === 'your-apify-token') {
34-
// eslint-disable-next-line no-param-reassign
35-
delete options.token;
53+
if (clientOptions.token?.toLowerCase() === 'your-apify-token') {
54+
delete clientOptions.token;
55+
}
56+
57+
// Handle authToken if provided
58+
if (authToken) {
59+
if (authToken.type === 'skyfire') {
60+
// For Skyfire tokens: DO NOT set as bearer token
61+
// Only add the skyfire-pay-id header via request interceptor
62+
// Remove any existing token to ensure no bearer auth
63+
delete clientOptions.token;
64+
} else {
65+
// For Apify tokens: Use as regular bearer token (existing behavior)
66+
clientOptions.token = authToken.value;
67+
}
68+
}
69+
70+
const requestInterceptors = [addUserAgent];
71+
if (authToken?.type === 'skyfire') {
72+
requestInterceptors.push((config) => addSkyfireHeader(config, authToken));
3673
}
3774

3875
super({
39-
...options,
76+
...clientOptions, // Now safe to spread without authToken
4077
baseUrl: getApifyAPIBaseUrl(),
41-
requestInterceptors: [addUserAgent],
78+
requestInterceptors,
4279
});
4380
}
4481
}

src/main.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import log from '@apify/log';
1111
import { createExpressApp } from './actor/server.js';
1212
import { processInput } from './input.js';
1313
import { callActorGetDataset } from './tools/index.js';
14-
import type { Input } from './types.js';
14+
import type { AuthToken, Input } from './types.js';
1515

1616
const STANDBY_MODE = Actor.getEnv().metaOrigin === 'STANDBY';
1717

@@ -25,6 +25,11 @@ if (!process.env.APIFY_TOKEN) {
2525
process.exit(1);
2626
}
2727

28+
const authToken: AuthToken = {
29+
value: process.env.APIFY_TOKEN,
30+
type: 'apify',
31+
};
32+
2833
const input = processInput((await Actor.getInput<Partial<Input>>()) ?? ({} as Input));
2934
log.info('Loaded input', { input: JSON.stringify(input) });
3035

@@ -44,7 +49,7 @@ if (STANDBY_MODE) {
4449
await Actor.fail('If you need to debug a specific Actor, please provide the debugActor and debugActorInput fields in the input');
4550
}
4651
const options = { memory: input.maxActorMemoryBytes } as ActorCallOptions;
47-
const { items } = await callActorGetDataset(input.debugActor!, input.debugActorInput!, process.env.APIFY_TOKEN, options);
52+
const { items } = await callActorGetDataset(input.debugActor!, input.debugActorInput!, authToken, options);
4853

4954
await Actor.pushData(items);
5055
log.info('Pushed items to dataset', { itemCount: items.count });

src/mcp/actors.ts

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { ActorDefinition } from 'apify-client';
22

3-
import { ApifyClient } from '../apify-client.js';
43
import { MCP_STREAMABLE_ENDPOINT } from '../const.js';
54
import type { ActorDefinitionPruned } from '../types.js';
65

@@ -44,38 +43,8 @@ export async function getActorMCPServerURL(realActorId: string, mcpServerPath: s
4443
}
4544

4645
/**
47-
* Gets Actor ID from the Actor object.
48-
*/
49-
export async function getRealActorID(actorIdOrName: string, apifyToken: string): Promise<string> {
50-
const apifyClient = new ApifyClient({ token: apifyToken });
51-
52-
const actor = apifyClient.actor(actorIdOrName);
53-
const info = await actor.get();
54-
if (!info) {
55-
throw new Error(`Actor ${actorIdOrName} not found`);
56-
}
57-
return info.id;
58-
}
59-
60-
/**
61-
* Returns standby URL for given Actor ID.
62-
*/
46+
* Returns standby URL for given Actor ID.
47+
*/
6348
export async function getActorStandbyURL(realActorId: string, standbyBaseUrl = 'apify.actor'): Promise<string> {
6449
return `https://${realActorId}.${standbyBaseUrl}`;
6550
}
66-
67-
export async function getActorDefinition(actorID: string, apifyToken: string): Promise<ActorDefinition> {
68-
const apifyClient = new ApifyClient({ token: apifyToken });
69-
const actor = apifyClient.actor(actorID);
70-
const defaultBuildClient = await actor.defaultBuild();
71-
const buildInfo = await defaultBuildClient.get();
72-
if (!buildInfo) {
73-
throw new Error(`Default build for Actor ${actorID} not found`);
74-
}
75-
const { actorDefinition } = buildInfo;
76-
if (!actorDefinition) {
77-
throw new Error(`Actor default build ${actorID} does not have Actor definition`);
78-
}
79-
80-
return actorDefinition;
81-
}

src/mcp/server.ts

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
import { prompts } from '../prompts/index.js';
3131
import { callActorGetDataset, defaultTools, getActorsAsTools, toolCategories } from '../tools/index.js';
3232
import { decodeDotPropertyNames } from '../tools/utils.js';
33-
import type { ActorMcpTool, ActorTool, HelperTool, ToolEntry } from '../types.js';
33+
import type { ActorMcpTool, ActorTool, AuthToken, HelperTool, ToolEntry } from '../types.js';
3434
import { createProgressTracker } from '../utils/progress.js';
3535
import { getToolPublicFieldOnly } from '../utils/tools.js';
3636
import { connectMCPClient } from './client.js';
@@ -160,7 +160,7 @@ export class ActorsMcpServer {
160160
* @param toolNames - Array of tool names to ensure are loaded
161161
* @param apifyToken - Apify API token for authentication
162162
*/
163-
public async loadToolsByName(toolNames: string[], apifyToken: string) {
163+
public async loadToolsByName(toolNames: string[], authToken: AuthToken) {
164164
const loadedTools = this.listAllToolNames();
165165
const actorsToLoad: string[] = [];
166166
const toolsToLoad: ToolEntry[] = [];
@@ -185,7 +185,7 @@ export class ActorsMcpServer {
185185
}
186186

187187
if (actorsToLoad.length > 0) {
188-
await this.loadActorsAsTools(actorsToLoad, apifyToken);
188+
await this.loadActorsAsTools(actorsToLoad, authToken);
189189
}
190190
}
191191

@@ -196,8 +196,8 @@ export class ActorsMcpServer {
196196
* @param apifyToken - Apify API token for authentication
197197
* @returns Promise<ToolEntry[]> - Array of loaded tool entries
198198
*/
199-
public async loadActorsAsTools(actorIdsOrNames: string[], apifyToken: string): Promise<ToolEntry[]> {
200-
const actorTools = await getActorsAsTools(actorIdsOrNames, apifyToken);
199+
public async loadActorsAsTools(actorIdsOrNames: string[], authToken: AuthToken): Promise<ToolEntry[]> {
200+
const actorTools = await getActorsAsTools(actorIdsOrNames, authToken);
201201
if (actorTools.length > 0) {
202202
this.upsertTools(actorTools, true);
203203
}
@@ -211,8 +211,8 @@ export class ActorsMcpServer {
211211
*
212212
* Used primarily for SSE.
213213
*/
214-
public async loadToolsFromUrl(url: string, apifyToken: string) {
215-
const tools = await processParamsGetTools(url, apifyToken);
214+
public async loadToolsFromUrl(url: string, authToken: AuthToken) {
215+
const tools = await processParamsGetTools(url, authToken);
216216
if (tools.length > 0) {
217217
log.debug('Loading tools from query parameters');
218218
this.upsertTools(tools, false);
@@ -381,17 +381,28 @@ export class ActorsMcpServer {
381381
// eslint-disable-next-line prefer-const
382382
let { name, arguments: args, _meta: meta } = request.params;
383383
const { progressToken } = meta || {};
384-
const apifyToken = (request.params.apifyToken || process.env.APIFY_TOKEN) as string;
384+
// Extract auth token with fallback to APIFY_TOKEN environment variable
385+
let authToken: AuthToken | undefined = request.params.authToken as AuthToken;
386+
387+
// Fallback to APIFY_TOKEN environment variable for local development
388+
if (!authToken && process.env.APIFY_TOKEN) {
389+
authToken = {
390+
value: process.env.APIFY_TOKEN,
391+
type: 'apify', // Environment variable is always an Apify token
392+
};
393+
}
385394
const userRentedActorIds = request.params.userRentedActorIds as string[] | undefined;
386395

387-
// Remove apifyToken from request.params just in case
388-
delete request.params.apifyToken;
396+
// Remove authToken from request.params only if it was provided in params
397+
if (request.params.authToken) {
398+
delete request.params.authToken;
399+
}
389400
// Remove other custom params passed from apify-mcp-server
390401
delete request.params.userRentedActorIds;
391402

392-
// Validate token
393-
if (!apifyToken) {
394-
const msg = 'APIFY_TOKEN is required. It must be set in the environment variables or passed as a parameter in the body.';
403+
// Validate auth token
404+
if (!authToken || !authToken.value) {
405+
const msg = 'Valid authentication token required. It must be provided either in the authToken parameter or APIFY_TOKEN environment variable.';
395406
log.error(msg);
396407
await this.server.sendLoggingMessage({ level: 'error', data: msg });
397408
throw new McpError(
@@ -462,7 +473,7 @@ export class ActorsMcpServer {
462473
extra,
463474
apifyMcpServer: this,
464475
mcpServer: this.server,
465-
apifyToken,
476+
authToken,
466477
userRentedActorIds,
467478
progressTracker,
468479
}) as object;
@@ -478,7 +489,7 @@ export class ActorsMcpServer {
478489
const serverTool = tool.tool as ActorMcpTool;
479490
let client: Client | undefined;
480491
try {
481-
client = await connectMCPClient(serverTool.serverUrl, apifyToken);
492+
client = await connectMCPClient(serverTool.serverUrl, authToken.value);
482493

483494
// Only set up notification handlers if progressToken is provided by the client
484495
if (progressToken) {
@@ -527,7 +538,7 @@ export class ActorsMcpServer {
527538
const { runId, datasetId, items } = await callActorGetDataset(
528539
actorTool.actorFullName,
529540
args,
530-
apifyToken as string,
541+
authToken,
531542
callOptions,
532543
progressTracker,
533544
);

src/mcp/utils.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
22
import { parse } from 'node:querystring';
33

44
import { processInput } from '../input.js';
5-
import type { Input } from '../types.js';
5+
import type { AuthToken, Input } from '../types.js';
66
import { loadToolsFromInput } from '../utils/tools-loader.js';
77
import { MAX_TOOL_NAME_LENGTH, SERVER_ID_LENGTH } from './const.js';
88

@@ -37,11 +37,11 @@ export function getProxyMCPServerToolName(url: string, toolName: string): string
3737
* Process input parameters from URL and get tools
3838
* If URL contains query parameter `actors`, return tools from Actors otherwise return null.
3939
* @param url
40-
* @param apifyToken
40+
* @param authToken
4141
*/
42-
export async function processParamsGetTools(url: string, apifyToken: string) {
42+
export async function processParamsGetTools(url: string, authToken: AuthToken) {
4343
const input = parseInputParamsFromUrl(url);
44-
return await loadToolsFromInput(input, apifyToken);
44+
return await loadToolsFromInput(input, authToken);
4545
}
4646

4747
export function parseInputParamsFromUrl(url: string): Input {

src/stdio.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import log from '@apify/log';
2424

2525
import { processInput } from './input.js';
2626
import { ActorsMcpServer } from './mcp/server.js';
27-
import type { Input, ToolSelector } from './types.js';
27+
import type { AuthToken, Input, ToolSelector } from './types.js';
2828
import { loadToolsFromInput } from './utils/tools-loader.js';
2929

3030
// Keeping this interface here and not types.ts since
@@ -122,7 +122,11 @@ async function main() {
122122
const normalized = processInput(input);
123123

124124
// Use the shared tools loading logic
125-
const tools = await loadToolsFromInput(normalized, process.env.APIFY_TOKEN as string);
125+
const authToken: AuthToken = {
126+
value: process.env.APIFY_TOKEN as string,
127+
type: 'apify',
128+
};
129+
const tools = await loadToolsFromInput(normalized, authToken);
126130

127131
mcpServer.upsertTools(tools);
128132

0 commit comments

Comments
 (0)