Skip to content

Commit 9a0846b

Browse files
committed
initial working draft of proxy actorized mcp servers
1 parent 0a836c9 commit 9a0846b

File tree

16 files changed

+291
-61
lines changed

16 files changed

+291
-61
lines changed

src/actor/server.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import express from 'express';
99
import log from '@apify/log';
1010

1111
import { HEADER_READINESS_PROBE, Routes } from './const.js';
12-
import { type ActorsMcpServer } from '../mcp-server.js';
13-
import { getActorRunData, processParamsGetTools } from './utils.js';
12+
import { type ActorsMcpServer } from '../mcp/mcp-server.js';
13+
import { getActorRunData } from './utils.js';
14+
import { processParamsGetTools } from '../mcp/utils.js';
1415

1516
export function createExpressApp(
1617
host: string,
@@ -32,7 +33,7 @@ export function createExpressApp(
3233
}
3334
try {
3435
log.info(`Received GET message at: ${Routes.ROOT}`);
35-
const tools = await processParamsGetTools(req.url);
36+
const tools = await processParamsGetTools(req.url, process.env.APIFY_TOKEN as string);
3637
if (tools) {
3738
mcpServer.updateTools(tools);
3839
}
@@ -53,7 +54,7 @@ export function createExpressApp(
5354
.get(async (req: Request, res: Response) => {
5455
try {
5556
log.info(`Received GET message at: ${Routes.SSE}`);
56-
const tools = await processParamsGetTools(req.url);
57+
const tools = await processParamsGetTools(req.url, process.env.APIFY_TOKEN as string);
5758
if (tools) {
5859
mcpServer.updateTools(tools);
5960
}

src/actor/types.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
export type Input = {
2-
actors: string[] | string;
3-
enableActorAutoLoading?: boolean;
4-
maxActorMemoryBytes?: number;
5-
debugActor?: string;
6-
debugActorInput?: unknown;
7-
};
81

92
export interface ActorRunData {
103
id?: string;

src/actor/utils.ts

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,7 @@
1-
import { parse } from 'node:querystring';
21

32
import { Actor } from 'apify';
3+
import type { ActorRunData } from './types.js';
44

5-
import { processInput } from './input.js';
6-
import type { ActorRunData, Input } from './types.js';
7-
import { addTool, getActorsAsTools, removeTool } from '../tools/index.js';
8-
import type { ToolWrap } from '../types.js';
9-
10-
export function parseInputParamsFromUrl(url: string): Input {
11-
const query = url.split('?')[1] || '';
12-
const params = parse(query) as unknown as Input;
13-
return processInput(params);
14-
}
15-
16-
/**
17-
* Process input parameters and get tools
18-
* If URL contains query parameter `actors`, return tools from Actors otherwise return null.
19-
* @param url
20-
*/
21-
export async function processParamsGetTools(url: string) {
22-
const input = parseInputParamsFromUrl(url);
23-
let tools: ToolWrap[] = [];
24-
if (input.actors) {
25-
tools = await getActorsAsTools(input.actors as string[]);
26-
}
27-
if (input.enableActorAutoLoading) {
28-
tools.push(addTool, removeTool);
29-
}
30-
return tools;
31-
}
325

336
export function getActorRunData(): ActorRunData | null {
347
return Actor.isAtHome() ? {

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
The ActorsMcpServer should be the only class exported from the package
44
*/
55

6-
import { ActorsMcpServer } from './mcp-server.js';
6+
import { ActorsMcpServer } from './mcp/mcp-server.js';
77
export default ActorsMcpServer;
File renamed without changes.

src/main.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import type { ActorCallOptions } from 'apify-client';
88

99
import log from '@apify/log';
1010

11-
import { processInput } from './actor/input.js';
11+
import { processInput } from './input.js';
1212
import { createExpressApp } from './actor/server.js';
13-
import type { Input } from './actor/types';
14-
import { ActorsMcpServer } from './mcp-server.js';
13+
import type { Input } from './types.js';
14+
import { ActorsMcpServer } from './mcp/mcp-server.js';
1515
import { actorDefinitionTool, addTool, removeTool, searchTool, callActorGetDataset } from './tools/index.js';
1616

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

src/mcp/actors.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
export async function isActorMCPServer(actorID: string): Promise<boolean> {
3+
// TODO: implement the logic
4+
return actorID === 'apify/actors-mcp-server';
5+
}
6+
7+
export async function getActorsMCPServerURL(actorID: string): Promise<string> {
8+
// TODO: implement the logic
9+
if (actorID === 'apify/actors-mcp-server') {
10+
return 'https://actors-mcp-server.apify.actor/sse';
11+
}
12+
throw new Error(`Actor ${actorID} is not an MCP server`);
13+
}

src/mcp/client.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
2+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
4+
import { getMCPServerID } from "./utils";
5+
6+
/**
7+
* Creates and connects a ModelContextProtocol client.
8+
*/
9+
export async function createMCPClient(
10+
url: string, token: string
11+
): Promise<Client> {
12+
const transport = new SSEClientTransport(
13+
new URL(url),
14+
{
15+
requestInit: {
16+
headers: {
17+
authorization: `Bearer ${token}`,
18+
},
19+
},
20+
eventSourceInit: {
21+
// The EventSource package augments EventSourceInit with a "fetch" parameter.
22+
// You can use this to set additional headers on the outgoing request.
23+
// Based on this example: https://github.com/modelcontextprotocol/typescript-sdk/issues/118
24+
async fetch(input: Request | URL | string, init?: RequestInit) {
25+
const headers = new Headers(init?.headers || {});
26+
headers.set('authorization', `Bearer ${token}`);
27+
return fetch(input, { ...init, headers });
28+
},
29+
// We have to cast to "any" to use it, since it's non-standard
30+
} as any, // eslint-disable-line @typescript-eslint/no-explicit-any
31+
});
32+
33+
const client = new Client({
34+
name: getMCPServerID(url),
35+
version: "1.0.0",
36+
});
37+
38+
await client.connect(transport);
39+
40+
return client;
41+
}

src/mcp/const.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
export const MAX_TOOL_NAME_LENGTH = 64;
3+
export const SERVER_ID_LENGTH = 16

src/mcp-server.ts renamed to src/mcp/mcp-server.ts

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
/**
23
* Model Context Protocol (MCP) server for Apify Actors
34
*/
@@ -14,12 +15,14 @@ import {
1415
ACTOR_OUTPUT_TRUNCATED_MESSAGE,
1516
SERVER_NAME,
1617
SERVER_VERSION,
17-
} from './const.js';
18-
import { actorDefinitionTool, callActorGetDataset, getActorsAsTools, searchTool } from './tools/index.js';
19-
import type { ActorTool, HelperTool, ToolWrap } from './types.js';
20-
import { defaults } from './const.js';
21-
import { actorNameToToolName } from './tools/utils.js';
22-
import { processParamsGetTools } from './actor/utils.js';
18+
} from '../const.js';
19+
import { actorDefinitionTool, callActorGetDataset, getActorsAsTools, searchTool } from '../tools/index.js';
20+
import type { ActorMCPTool, ActorTool, HelperTool, ToolWrap } from '../types.js';
21+
import { defaults } from '../const.js';
22+
import { actorNameToToolName } from '../tools/utils.js';
23+
import { processParamsGetTools } from './utils.js';
24+
import { createMCPClient } from './client.js';
25+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2326

2427
/**
2528
* Create Apify MCP server
@@ -51,9 +54,9 @@ export class ActorsMcpServer {
5154
/**
5255
* Loads missing default tools.
5356
*/
54-
public async loadDefaultTools() {
57+
public async loadDefaultTools(apifyToken: string) {
5558
const missingDefaultTools = defaults.actors.filter(name => !this.tools.has(actorNameToToolName(name)));
56-
const tools = await getActorsAsTools(missingDefaultTools);
59+
const tools = await getActorsAsTools(missingDefaultTools, apifyToken);
5760
if (tools.length > 0) this.updateTools(tools);
5861
}
5962

@@ -62,8 +65,8 @@ export class ActorsMcpServer {
6265
*
6366
* Used primarily for SSE.
6467
*/
65-
public async loadToolsFromUrl(url: string) {
66-
const tools = await processParamsGetTools(url);
68+
public async loadToolsFromUrl(url: string, apifyToken: string) {
69+
const tools = await processParamsGetTools(url, apifyToken);
6770
if (tools.length > 0) this.updateTools(tools);
6871
}
6972

@@ -117,7 +120,10 @@ export class ActorsMcpServer {
117120
*/
118121
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
119122
const { name, arguments: args } = request.params;
120-
const apifyToken = request.params.apifyToken || process.env.APIFY_TOKEN;
123+
const apifyToken = (request.params.apifyToken || process.env.APIFY_TOKEN) as string;
124+
125+
// Remove apifyToken from request.params just in case
126+
delete request.params.apifyToken;
121127

122128
// Validate token
123129
if (!apifyToken) {
@@ -147,11 +153,29 @@ export class ActorsMcpServer {
147153
args,
148154
apifyMcpServer: this,
149155
mcpServer: this.server,
156+
apifyToken,
150157
}) as object;
151158

152159
return { ...res };
153160
}
154161

162+
if (tool.type === 'actor-mcp') {
163+
const serverTool = tool.tool as ActorMCPTool;
164+
let client: Client | undefined;
165+
try {
166+
client = await createMCPClient(serverTool.serverUrl, apifyToken);
167+
const res = await client.callTool({
168+
name: name,
169+
arguments: args,
170+
});
171+
172+
return { ...res };
173+
} finally {
174+
if (client) await client.close();
175+
}
176+
177+
}
178+
155179
// Handle actor tool
156180
if (tool.type === 'actor') {
157181
const actorTool = tool.tool as ActorTool;

0 commit comments

Comments
 (0)