mcp template
Pricing
Pay per usage
Go to Apify Store
mcp template
Under maintenance0.0 (0)
Pricing
Pay per usage
0
3
2
Last modified
a month ago
Pricing
Pay per usage
0.0 (0)
Pricing
Pay per usage
0
3
2
Last modified
a month ago
{    "actorSpecification": 1,    "name": "my-actor-7",    "title": "TypeScript MCP server",    "description": "TypeScript Model Context Protocol server.",    "version": "0.0",    "buildTag": "latest",    "usesStandbyMode": true,    "meta": {        "templateId": "ts-mcp-server"    },    "input": {        "title": "Actor input schema",        "description": "This is Actor input schema",        "type": "object",        "schemaVersion": 1,        "properties": {},        "required": []    },    "dockerfile": "../Dockerfile",    "webServerMcpPath": "/mcp"}{    "actor-start": {        "eventTitle": "Price for Actor start",        "eventDescription": "Flat fee for starting an Actor run.",        "eventPriceUsd": 0.1    },    "tool-request": {        "eventTitle": "Price for completing a tool request",        "eventDescription": "Flat fee for completing a tool request.",        "eventPriceUsd": 0.05    },    "prompt-request": {        "eventTitle": "Price for completing a prompt request",        "eventDescription": "Flat fee for completing a prompt request.",        "eventPriceUsd": 0.01    },    "completion-request": {        "eventTitle": "Price for completing a completion request",        "eventDescription": "Flat fee for completing a completion request.",        "eventPriceUsd": 0.001    },    "resource-request": {        "eventTitle": "Price for completing a resource request",        "eventDescription": "Flat fee for completing a resource request.",        "eventPriceUsd": 0.025    },    "list-request": {        "eventTitle": "Price for completing a list request",        "eventDescription": "Flat fee for completing a list request.",        "eventPriceUsd": 0.001    }}# configurations.idea.vscode.zed
# crawlee and apify storage foldersapify_storagecrawlee_storagestorage
# installed filesnode_modules
# git folder.git
# dist folderdistroot = true
[*]indent_style = spaceindent_size = 4charset = utf-8trim_trailing_whitespace = trueinsert_final_newline = trueend_of_line = lf# This file tells Git which files shouldn't be added to source control
.idea.vscode.zedstorageapify_storagecrawlee_storagenode_modulesdisttsconfig.tsbuildinfo
# Added by Apify CLI.venv.prettierignore{    "printWidth": 120,    "singleQuote": true,    "tabWidth": 4}# Specify the base Docker image. You can read more about# the available images at https://docs.apify.com/sdk/js/docs/guides/docker-images# You can also use any other image from Docker Hub.FROM apify/actor-node:22 AS builder
# Check preinstalled packagesRUN npm ls crawlee apify puppeteer playwright
# Copy just package.json and package-lock.json# to speed up the build using Docker layer cache.COPY  package*.json ./
# Install all dependencies. Don't audit to speed up the installation.RUN npm install --include=dev --audit=false
# Next, copy the source files using the user set# in the base image.COPY  . ./
# Install all dependencies and build the project.# Don't audit to speed up the installation.RUN npm run build
# Create final imageFROM apify/actor-node:22
# Check preinstalled packagesRUN npm ls crawlee apify puppeteer playwright
# Copy just package.json and package-lock.json# to speed up the build using Docker layer cache.COPY  package*.json ./
# Install NPM packages, skip optional and development dependencies to# keep the image small. Avoid logging too much and print the dependency# tree for debuggingRUN npm --quiet set progress=false \    && npm install --omit=dev --omit=optional \    && echo "Installed NPM packages:" \    && (npm list --omit=dev --all || true) \    && echo "Node.js version:" \    && node --version \    && echo "NPM version:" \    && npm --version \    && rm -r ~/.npm
# Copy built JS files from builder imageCOPY  /usr/src/app/dist ./dist
# Next, copy the remaining files and directories with the source code.# Since we do this after NPM install, quick build will be really fast# for most source file changes.COPY  . ./
# Run the image.CMD npm run start:prod --silent1import prettier from 'eslint-config-prettier';2
3import apify from '@apify/eslint-config/ts.js';4import globals from 'globals';5import tsEslint from 'typescript-eslint';6
7// eslint-disable-next-line import/no-default-export8export default [9    { ignores: ['**/dist', 'eslint.config.mjs'] },10    ...apify,11    prettier,12    {13        languageOptions: {14            parser: tsEslint.parser,15            parserOptions: {16                project: 'tsconfig.json',17            },18            globals: {19                ...globals.node,20                ...globals.jest,21            },22        },23        plugins: {24            '@typescript-eslint': tsEslint.plugin,25        },26        rules: {27            'no-console': 0,28        },29    },30];{    "name": "ts-mcp-server",    "version": "0.0.1",    "type": "module",    "description": "TypeScript Model Context Protocol server.",    "engines": {        "node": ">=20.0.0"    },    "dependencies": {        "@modelcontextprotocol/sdk": "^1.17.5",        "@modelcontextprotocol/server-everything": "^2025.8.18",        "mcp-remote": "0.1.29",        "apify": "^3.4.4",        "body-parser": "^2.2.0",        "express": "^5.1.0",        "zod": "^3.25.76"    },    "devDependencies": {        "@apify/eslint-config": "^1.0.0",        "@apify/tsconfig": "^0.1.1",        "@types/body-parser": "^1.19.5",        "@types/express": "^5.0.2",        "@types/node": "^22.15.32",        "eslint": "^9.29.0",        "eslint-config-prettier": "^10.1.5",        "globals": "^16.2.0",        "prettier": "^3.5.3",        "tsx": "^4.20.3",        "typescript": "^5.8.3",        "typescript-eslint": "^8.34.1"    },    "scripts": {        "start": "npm run start:dev",        "start:prod": "node dist/main.js",        "start:dev": "tsx src/main.ts",        "build": "tsc",        "test": "echo \"Error: oops, the Actor has no tests yet, sad!\" && exit 1"    },    "author": "It's not you it's me",    "license": "ISC"}1/**2 * This module handles billing for different types of protocol requests in the MCP server.3 * It defines a function to charge users based on the type of protocol method invoked.4 */5import { Actor, log } from 'apify';6
7/**8 * Charges the user for a message request based on the method type.9 * Supported method types are mapped to specific billing events.10 *11 * @param request - The request object containing the method string.12 * @returns Promise<void>13 */14export async function chargeMessageRequest(request: { method: string }): Promise<void> {15    const { method } = request;16
17    // See https://modelcontextprotocol.io/specification/2025-06-18/server for more details18    // on the method names and protocol messages19    // Charge for list requests (e.g., tools/list, resources/list, etc.)20    if (method.endsWith('/list')) {21        await Actor.charge({ eventName: 'list-request' });22        log.info(`Charged for list request: ${method}`);23        // Charge for tool-related requests24    } else if (method.startsWith('tools/')) {25        await Actor.charge({ eventName: 'tool-request' });26        log.info(`Charged for tool request: ${method}`);27        // Charge for resource-related requests28    } else if (method.startsWith('resources/')) {29        await Actor.charge({ eventName: 'resource-request' });30        log.info(`Charged for resource request: ${method}`);31        // Charge for prompt-related requests32    } else if (method.startsWith('prompts/')) {33        await Actor.charge({ eventName: 'prompt-request' });34        log.info(`Charged for prompt request: ${method}`);35        // Charge for completion-related requests36    } else if (method.startsWith('completion/')) {37        await Actor.charge({ eventName: 'completion-request' });38        log.info(`Charged for completion request: ${method}`);39        // Do not charge for other methods40    } else {41        log.info(`Not charging for method: ${method}`);42    }43}1/**2 * MCP Server - Main Entry Point3 *4 * This file serves as the entry point for the MCP Server Actor.5 * It sets up a proxy server that forwards requests to the locally running6 * MCP server, which provides a Model Context Protocol (MCP) interface.7 */8
9// Apify SDK - toolkit for building Apify Actors (Read more at https://docs.apify.com/sdk/js/)10import { Actor, log } from 'apify';11
12import { startServer } from './server.js';13
14// This is an ESM project, and as such, it requires you to specify extensions in your relative imports15// Read more about this here: https://nodejs.org/docs/latest-v18.x/api/esm.html#mandatory-file-extensions16// Note that we need to use `.js` even when inside TS files17// import { router } from './routes.js';18
19// Configuration constants for the MCP server20// Command to run the Everything MCP Server21// TODO: Do not forget to install the MCP server in package.json (using `npm install ...`)22const MCP_COMMAND = ['npx', '@modelcontextprotocol/server-everything'];23
24// Check if the Actor is running in standby mode25const STANDBY_MODE = process.env.APIFY_META_ORIGIN === 'STANDBY';26const SERVER_PORT = parseInt(process.env.ACTOR_WEB_SERVER_PORT || '3001', 10);27
28// Initialize the Apify Actor environment29// The init() call configures the Actor for its environment. It's recommended to start every Actor with an init()30await Actor.init();31
32// Charge for Actor start33await Actor.charge({ eventName: 'actor-start' });34
35if (!STANDBY_MODE) {36    // If the Actor is not in standby mode, we should not run the MCP server37    const msg = 'This Actor is not meant to be run directly. It should be run in standby mode.';38    log.error(msg);39    await Actor.exit({ statusMessage: msg });40}41
42await startServer({43    serverPort: SERVER_PORT,44    command: MCP_COMMAND,45});1/**2 * This module provides functions to create and manage an MCP server and its proxy client.3 * It registers protocol capabilities, request handlers, and notification handlers for the MCP server,4 * and spawns a proxy client that communicates with another MCP process over stdio.5 */6import { Client } from '@modelcontextprotocol/sdk/client/index.js';7import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';8import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';9import { DEFAULT_REQUEST_TIMEOUT_MSEC } from '@modelcontextprotocol/sdk/shared/protocol.js';10import {11    ClientNotificationSchema,12    ClientRequestSchema,13    ResultSchema,14    ServerNotificationSchema,15} from '@modelcontextprotocol/sdk/types.js';16import { log } from 'apify';17
18/**19 * Creates and configures an MCP server instance.20 *21 * - Registers all protocol capabilities except experimental.22 * - Spawns a proxy client to forward requests and notifications.23 * - Sets up handlers for requests and notifications between the server and proxy client.24 * - Handles server shutdown and proxy client cleanup.25 *26 * @param command - The command to start the MCP proxy process.27 * @param options - Optional configuration (e.g., request timeout).28 * @returns A Promise that resolves to a configured McpServer instance.29 */30export async function getMcpServer(31    command: string[],32    options?: {33        timeout?: number;34    },35): Promise<McpServer> {36    // Create the MCP server instance37    const server = new McpServer({38        name: 'mcp-server',39        version: '1.0.0',40    });41
42    // Register all capabilities except experimental43    server.server.registerCapabilities({44        tools: {},45        prompts: {},46        resources: {},47        completions: {},48        logging: {},49    });50
51    // Spawn MCP proxy client for the stdio MCP server52    const proxyClient = await getMcpProxyClient(command);53
54    // Register request handlers for all client requests55    for (const schema of ClientRequestSchema.options) {56        const method = schema.shape.method.value;57        // Forward requests to the proxy client and return its response58        server.server.setRequestHandler(schema, async (req) => {59            if (req.method === 'initialize') {60                // Handle the 'initialize' request separately and do not forward it to the proxy client61                // this is needed for mcp-remote servers to work correctly62                return {63                    capabilities: proxyClient.getServerCapabilities(),64                    // Return back the client protocolVersion65                    protocolVersion: req.params.protocolVersion,66                    serverInfo: {67                        name: 'Apify MCP proxy server',68                        title: 'Apify MCP proxy server',69                        version: '1.0.0',70                    },71                };72            }73            log.info('Received MCP request', {74                method,75                request: req,76            });77            return proxyClient.request(req, ResultSchema, {78                timeout: options?.timeout || DEFAULT_REQUEST_TIMEOUT_MSEC,79            });80        });81    }82
83    // Register notification handlers for all client notifications84    for (const schema of ClientNotificationSchema.options) {85        const method = schema.shape.method.value;86        // Forward notifications to the proxy client87        server.server.setNotificationHandler(schema, async (notification) => {88            if (notification.method === 'notifications/initialized') {89                // Do not forward the 'notifications/initialized' notification90                // This is needed for mcp-remote servers to work correctly91                return;92            }93            log.info('Received MCP notification', {94                method,95                notification,96            });97            await proxyClient.notification(notification);98        });99    }100
101    // Register notification handlers for all proxy client notifications102    for (const schema of ServerNotificationSchema.options) {103        const method = schema.shape.method.value;104        // Forward notifications from the proxy client to the server105        proxyClient.setNotificationHandler(schema, async (notification) => {106            log.info('Sending MCP notification', {107                method,108                notification,109            });110            await server.server.notification(notification);111        });112    }113
114    // Handle server shutdown and cleanup proxy client115    server.server.onclose = () => {116        log.info('MCP Server is closing, shutting down the proxy client');117        proxyClient.close().catch((error) => {118            log.error('Error closing MCP Proxy Client', {119                error,120            });121        });122    };123
124    return server;125}126
127/**128 * Creates and connects an MCP Proxy Client using a given command.129 *130 * This function splits the provided command string into the executable and its arguments,131 * initializes a StdioClientTransport for communication, and then creates a Client instance.132 * It connects the client to the transport and returns the connected client.133 *134 * @param command - The command to start the MCP proxy process (e.g., 'node server.js').135 * @returns A Promise that resolves to a connected Client instance.136 */137export async function getMcpProxyClient(command: string[]): Promise<Client> {138    log.info('Starting MCP Proxy Client', {139        command,140    });141    // Create a stdio transport for the proxy client142    const transport = new StdioClientTransport({143        command: command[0],144        args: command.slice(1),145    });146
147    // Create the MCP proxy client instance148    const client = new Client({149        name: 'mcp-proxy-client',150        version: '1.0.0',151    });152
153    // Connect the client to the transport154    await client.connect(transport);155    log.info('MCP Proxy Client started successfully');156    return client;157}1/**2 * This module implements the HTTP server for the MCP protocol.3 * It manages session-based transports, request routing, and billing for protocol messages.4 *5 * The server supports streamable HTTP endpoints and handles session6 * initialization, message routing, and resource cleanup on shutdown.7 */8import { randomUUID } from 'node:crypto';9
10import { InMemoryEventStore } from '@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js';11import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';12import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';13import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';14import { log } from 'apify';15import type { Request, Response } from 'express';16import express from 'express';17
18import { chargeMessageRequest } from './billing.js';19import { getMcpServer as getMCPServerWithCommand } from './mcp.js';20
21let getMcpServer: null | (() => Promise<McpServer>) = null;22
23// Map to store transports by session ID24const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};25
26/**27 * Handles POST requests to the /mcp endpoint.28 * - Initializes new sessions and transports if needed.29 * - Routes requests to the correct transport based on session ID.30 * - Charges for each message request.31 */32async function mcpPostHandler(req: Request, res: Response) {33    // Ensure the MCP server is initialized34    if (!getMcpServer) {35        res.status(500).json({36            jsonrpc: '2.0',37            error: {38                code: -32000,39                message: 'Server not initialized',40            },41            id: null,42        });43        return;44    }45    const sessionId = req.headers['mcp-session-id'] as string | undefined;46    log.info('Received MCP request', {47        sessionId: sessionId || null,48        body: req.body,49    });50    try {51        let transport: StreamableHTTPServerTransport;52        if (sessionId && transports[sessionId]) {53            // Reuse existing transport for the session54            transport = transports[sessionId] as StreamableHTTPServerTransport;55        } else if (!sessionId && isInitializeRequest(req.body)) {56            // New initialization request: create a new transport and event store57            const eventStore = new InMemoryEventStore();58            transport = new StreamableHTTPServerTransport({59                sessionIdGenerator: () => randomUUID(),60                eventStore, // Enable resumability61                onsessioninitialized: (initializedSessionId) => {62                    // Store the transport by session ID when session is initialized63                    // This avoids race conditions where requests might come in before the session is stored64                    log.info('Session initialized', {65                        sessionId: initializedSessionId,66                    });67                    transports[initializedSessionId] = transport;68                },69            });70
71            // Charge for each message request received on this transport72            transport.onmessage = (message) => {73                chargeMessageRequest(message as { method: string }).catch((error) => {74                    log.error('Error charging for message request:', {75                        error,76                        sessionId: transport.sessionId || null,77                    });78                });79            };80
81            // Clean up transport when closed82            transport.onclose = () => {83                const sid = transport.sessionId;84                if (sid && transports[sid]) {85                    log.info('Transport closed', {86                        sessionId: sid,87                    });88                    delete transports[sid];89                }90            };91
92            // Connect the transport to the MCP server BEFORE handling the request93            // so responses can flow back through the same transport94            const server = await getMcpServer();95            await server.connect(transport);96
97            await transport.handleRequest(req, res, req.body);98            return; // Already handled99        } else {100            // Invalid request - no session ID or not initialization request101            res.status(400).json({102                jsonrpc: '2.0',103                error: {104                    code: -32000,105                    message: 'Bad Request: No valid session ID provided',106                },107                id: null,108            });109            return;110        }111
112        // Handle the request with existing transport - no need to reconnect113        // The existing transport is already connected to the server114        await transport.handleRequest(req, res, req.body);115    } catch (error) {116        log.error('Error handling MCP request:', {117            error,118            sessionId: sessionId || null,119        });120        if (!res.headersSent) {121            res.status(500).json({122                jsonrpc: '2.0',123                error: {124                    code: -32603,125                    message: 'Internal server error',126                },127                id: null,128            });129        }130    }131}132
133/**134 * Handles GET requests to the /mcp endpoint for streaming responses.135 * - Validates session ID and resumes or establishes SSE streams as needed.136 */137async function mcpGetHandler(req: Request, res: Response) {138    const sessionId = req.headers['mcp-session-id'] as string | undefined;139    if (!sessionId || !transports[sessionId]) {140        res.status(400).send('Invalid or missing session ID');141        return;142    }143
144    // Check for Last-Event-ID header for resumability145    const lastEventId = req.headers['last-event-id'] as string | undefined;146    if (lastEventId) {147        log.info('Client reconnecting', {148            lastEventId: lastEventId || null,149        });150    } else {151        log.info('Establishing new SSE stream', {152            sessionId: sessionId || null,153        });154    }155
156    const transport = transports[sessionId] as StreamableHTTPServerTransport;157    await transport.handleRequest(req, res);158}159
160/**161 * Handles DELETE requests to the /mcp endpoint for session termination.162 * - Cleans up and closes the transport for the given session.163 */164async function mcpDeleteHandler(req: Request, res: Response) {165    const sessionId = req.headers['mcp-session-id'] as string | undefined;166    if (!sessionId || !transports[sessionId]) {167        res.status(400).send('Invalid or missing session ID');168        return;169    }170
171    log.info('Received session termination request', {172        sessionId: sessionId || null,173    });174
175    try {176        const transport = transports[sessionId] as StreamableHTTPServerTransport;177        await transport.handleRequest(req, res);178    } catch (error) {179        log.error('Error handling session termination:', {180            error,181        });182        if (!res.headersSent) {183            res.status(500).send('Error processing session termination');184        }185    }186}187
188/**189 * Starts the MCP HTTP server and sets up all endpoints.190 * - Initializes the MCP server factory.191 * - Registers all HTTP endpoints.192 * - Handles graceful shutdown and resource cleanup.193 */194export async function startServer(options: { serverPort: number; command: string[] }) {195    log.info('Starting MCP HTTP Server', {196        serverPort: options.serverPort,197        command: options.command,198    });199    const { serverPort, command } = options;200    // Initialize the MCP client201    getMcpServer = async () => getMCPServerWithCommand(command);202
203    const app = express();204
205    // Redirect to Apify favicon206    app.get('/favicon.ico', (_req: Request, res: Response) => {207        res.writeHead(301, { Location: 'https://apify.com/favicon.ico' });208        res.end();209    });210
211    // Readiness probe handler212    app.get('/', (req: Request, res: Response) => {213        if (req.headers['x-apify-container-server-readiness-probe']) {214            console.log('Readiness probe');215            res.end('ok\n');216            return;217        }218        res.status(404).end();219    });220
221    // Return the Apify OAuth authorization server metadata to allow the MCP client to authenticate using OAuth222    app.get('/.well-known/oauth-authorization-server', async (_req: Request, res: Response) => {223        // Some MCP clients do not follow redirects, so we need to fetch the data and return it directly.224        const response = await fetch(`https://api.apify.com/.well-known/oauth-authorization-server`);225        const data = await response.json();226        res.status(200).json(data);227    });228
229    app.use(express.json());230
231    // Streamable HTTP endpoints232    app.post('/mcp', mcpPostHandler);233    app.get('/mcp', mcpGetHandler);234    app.delete('/mcp', mcpDeleteHandler);235
236    app.listen(serverPort, () => {237        log.info(`MCP HTTP Server listening on port ${serverPort}`);238    });239
240    // Handle server shutdown241    process.on('SIGINT', async () => {242        log.info('Shutting down server...');243
244        // Close all active transports to properly clean up resources245        for (const sessionId of Object.keys(transports)) {246            try {247                log.info(`Closing transport for session ${sessionId}`);248                await transports[sessionId].close();249                delete transports[sessionId];250            } catch (error) {251                log.error(`Error closing transport for session ${sessionId}:`, {252                    error,253                });254            }255        }256        log.info('Server shutdown complete');257        process.exit(0);258    });259}{    "extends": "@apify/tsconfig",    "compilerOptions": {        "module": "NodeNext",        "moduleResolution": "NodeNext",        "target": "ES2022",        "outDir": "dist",        "noUnusedLocals": false,        "skipLibCheck": true,        "lib": ["DOM"]    },    "include": ["./src/**/*"]}