Skip to content

Commit bb1c10b

Browse files
committed
wip snapshot, decouple logic, split stdio cli, http server and prepare
library entrypoint
1 parent 0e5ba2e commit bb1c10b

File tree

9 files changed

+419
-352
lines changed

9 files changed

+419
-352
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
},
99
"main": "dist/index.js",
1010
"bin": {
11-
"actors-mcp-server": "./dist/index.js"
11+
"actors-mcp-server": "./dist/stdio.js"
1212
},
1313
"files": [
1414
"dist",

src/examples/clientSse.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ const dirname = path.dirname(filename);
2323

2424
dotenv.config({ path: path.resolve(dirname, '../../.env') });
2525

26-
const SERVER_URL = 'https://actors-mcp-server.apify.actor/sse';
26+
const SERVER_URL = process.env.MCP_SERVER_URL_BASE || 'https://actors-mcp-server.apify.actor/sse';
2727
// We need to change forward slash / to underscore -- in the tool name as Anthropic does not allow forward slashes in the tool name
2828
const SELECTED_TOOL = actorNameToToolName('apify/rag-web-browser');
29-
const QUERY = 'web browser for Anthropic';
29+
//const QUERY = 'web browser for Anthropic';
30+
const QUERY = 'apify';
3031

3132
if (!process.env.APIFY_TOKEN) {
3233
console.error('APIFY_TOKEN is required but not set in the environment variables.');
@@ -98,6 +99,8 @@ async function main(): Promise<void> {
9899
} else {
99100
console.error('An unknown error occurred:', error);
100101
}
102+
} finally {
103+
await client.close();
101104
}
102105
}
103106

src/index.ts

Lines changed: 4 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,4 @@
1-
#!/usr/bin/env node
2-
/**
3-
* This script initializes and starts the Apify MCP server using the Stdio transport.
4-
*
5-
* Usage:
6-
* node <script_name> --actors=<actor1,actor2,...>
7-
*
8-
* Command-line arguments:
9-
* --actors - A comma-separated list of actor full names to add to the server.
10-
*
11-
* Example:
12-
* node index.js --actors=apify/google-search-scraper,apify/instagram-scraper
13-
*/
14-
15-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
16-
import minimist from 'minimist';
17-
18-
import { log } from './logger.js';
19-
import { ApifyMcpServer } from './server.js';
20-
import { getActorDiscoveryTools, getActorAutoLoadingTools } from './tools.js';
21-
22-
log.setLevel(log.LEVELS.ERROR);
23-
24-
const argv = minimist(process.argv.slice(2));
25-
const argActors = argv.actors?.split(',').map((actor: string) => actor.trim()) || [];
26-
const argEnableActorAutoLoading = argv.enableActorAutoLoading || false;
27-
28-
if (!process.env.APIFY_TOKEN) {
29-
log.error('APIFY_TOKEN is required but not set in the environment variables.');
30-
process.exit(1);
31-
}
32-
33-
async function main() {
34-
const server = new ApifyMcpServer();
35-
await (argActors.length !== 0
36-
? server.addToolsFromActors(argActors)
37-
: server.addToolsFromDefaultActors());
38-
server.updateTools(getActorDiscoveryTools());
39-
if (argEnableActorAutoLoading) {
40-
server.updateTools(getActorAutoLoadingTools());
41-
}
42-
const transport = new StdioServerTransport();
43-
await server.connect(transport);
44-
}
45-
46-
main().catch((error) => {
47-
console.error('Server error:', error); // eslint-disable-line no-console
48-
process.exit(1);
49-
});
1+
/*
2+
This file provides essential functions for constructing HTTP and MCP servers, effectively serving as a library.
3+
Acts as a library entrypoint.
4+
*/

src/main.ts

Lines changed: 14 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
import type { ParsedUrlQuery } from 'querystring';
2-
import { parse } from 'querystring';
1+
/*
2+
This file serves as an Actor MCP SSE server entry point.
3+
*/
4+
5+
import type { ParsedUrlQuery } from 'node:querystring';
6+
import { parse } from 'node:querystring';
37

48
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
59
import { Actor } from 'apify';
@@ -10,30 +14,25 @@ import express from 'express';
1014
import { HEADER_READINESS_PROBE, Routes } from './const.js';
1115
import { processInput } from './input.js';
1216
import { log } from './logger.js';
13-
import { ApifyMcpServer } from './server.js';
17+
import { ApifyMcpServer } from './mcp-server.js';
18+
import { createServerApp } from './server.js';
1419
import { getActorDiscoveryTools, getActorAutoLoadingTools } from './tools.js';
1520
import type { Input } from './types.js';
21+
import { isActorStandby } from './utils.js';
1622

1723
await Actor.init();
1824

19-
const STANDBY_MODE = Actor.getEnv().metaOrigin === 'STANDBY';
20-
const HOST = Actor.isAtHome() ? process.env.ACTOR_STANDBY_URL : 'http://localhost';
21-
const PORT = Actor.isAtHome() ? process.env.ACTOR_STANDBY_PORT : 3001;
25+
const HOST = Actor.isAtHome() ? process.env.ACTOR_STANDBY_URL as string : 'http://localhost';
26+
const PORT = Actor.isAtHome() ? Number(process.env.ACTOR_STANDBY_PORT) : 3001;
2227

2328
if (!process.env.APIFY_TOKEN) {
2429
log.error('APIFY_TOKEN is required but not set in the environment variables.');
2530
process.exit(1);
2631
}
2732

28-
const app = express();
29-
3033
const mcpServer = new ApifyMcpServer();
31-
let transport: SSEServerTransport;
3234

33-
const HELP_MESSAGE = `Connect to the server with GET request to ${HOST}/sse?token=YOUR-APIFY-TOKEN`
34-
+ ` and then send POST requests to ${HOST}/message?token=YOUR-APIFY-TOKEN`;
35-
36-
const actorRun = Actor.isAtHome() ? {
35+
const actorRunData = Actor.isAtHome() ? {
3736
id: process.env.ACTOR_RUN_ID,
3837
actId: process.env.ACTOR_ID,
3938
userId: process.env.APIFY_USER_ID,
@@ -56,86 +55,11 @@ const actorRun = Actor.isAtHome() ? {
5655
standbyUrl: process.env.ACTOR_STANDBY_URL,
5756
} : {};
5857

59-
/**
60-
* Process input parameters and update tools
61-
* If URL contains query parameter actors, add tools from actors, otherwise add tools from default actors
62-
* @param url
63-
*/
64-
async function processParamsAndUpdateTools(url: string) {
65-
const params = parse(url.split('?')[1] || '') as ParsedUrlQuery;
66-
delete params.token;
67-
log.debug(`Received input parameters: ${JSON.stringify(params)}`);
68-
const input = await processInput(params as unknown as Input);
69-
if (input.actors) {
70-
await mcpServer.addToolsFromActors(input.actors as string[]);
71-
}
72-
if (input.enableActorAutoLoading) {
73-
mcpServer.updateTools(getActorAutoLoadingTools());
74-
}
75-
log.debug(`Server is running in STANDBY mode with the following Actors (tools): ${mcpServer.getToolNames()}.
76-
To use different Actors, provide them in query parameter "actors" or include them in the Actor Task input.`);
77-
}
78-
79-
app.route(Routes.ROOT)
80-
.get(async (req: Request, res: Response) => {
81-
if (req.headers && req.get(HEADER_READINESS_PROBE) !== undefined) {
82-
log.debug('Received readiness probe');
83-
res.status(200).json({ message: 'Server is ready' }).end();
84-
return;
85-
}
86-
try {
87-
log.info(`Received GET message at: ${Routes.ROOT}`);
88-
await processParamsAndUpdateTools(req.url);
89-
res.status(200).json({ message: `Actor is using Model Context Protocol. ${HELP_MESSAGE}`, data: actorRun }).end();
90-
} catch (error) {
91-
log.error(`Error in GET ${Routes.ROOT} ${error}`);
92-
res.status(500).json({ message: 'Internal Server Error' }).end();
93-
}
94-
})
95-
.head((_req: Request, res: Response) => {
96-
res.status(200).end();
97-
});
98-
99-
app.route(Routes.SSE)
100-
.get(async (req: Request, res: Response) => {
101-
try {
102-
log.info(`Received GET message at: ${Routes.SSE}`);
103-
await processParamsAndUpdateTools(req.url);
104-
transport = new SSEServerTransport(Routes.MESSAGE, res);
105-
await mcpServer.connect(transport);
106-
} catch (error) {
107-
log.error(`Error in GET ${Routes.SSE}: ${error}`);
108-
res.status(500).json({ message: 'Internal Server Error' }).end();
109-
}
110-
});
111-
112-
app.route(Routes.MESSAGE)
113-
.post(async (req: Request, res: Response) => {
114-
try {
115-
log.info(`Received POST message at: ${Routes.MESSAGE}`);
116-
if (transport) {
117-
await transport.handlePostMessage(req, res);
118-
} else {
119-
res.status(400).json({
120-
message: 'Server is not connected to the client. '
121-
+ 'Connect to the server with GET request to /sse endpoint',
122-
});
123-
}
124-
} catch (error) {
125-
log.error(`Error in POST ${Routes.MESSAGE}: ${error}`);
126-
res.status(500).json({ message: 'Internal Server Error' }).end();
127-
}
128-
});
129-
130-
// Catch-all for undefined routes
131-
app.use((req: Request, res: Response) => {
132-
res.status(404).json({ message: `There is nothing at route ${req.method} ${req.originalUrl}. ${HELP_MESSAGE}` }).end();
133-
});
134-
13558
const input = await processInput((await Actor.getInput<Partial<Input>>()) ?? ({} as Input));
13659
log.info(`Loaded input: ${JSON.stringify(input)} `);
13760

138-
if (STANDBY_MODE) {
61+
if (isActorStandby()) {
62+
const app = createServerApp(HOST, mcpServer, actorRunData);
13963
log.info('Actor is running in the STANDBY mode.');
14064
await mcpServer.addToolsFromDefaultActors();
14165
mcpServer.updateTools(getActorDiscoveryTools());

0 commit comments

Comments
 (0)