mcp template
Pricing
Pay per usage
Go to Apify Store
mcp template
0.0 (0)
Pricing
Pay per usage
0
3
2
Last modified
7 days ago
Pricing
Pay per usage
0.0 (0)
Pricing
Pay per usage
0
3
2
Last modified
7 days 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 folderdist
root = 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 --silent
1import 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/**/*"]}