TypeScript MCP server
Create a Model Context Protocol server using TypeScript and Express with Apify Actor integration for pay-per-event monetization.
src/main.ts
1import express, { Request, Response } from 'express';2import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';3import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';4import * as z from 'zod';5import { CallToolResult, ReadResourceResult } from '@modelcontextprotocol/sdk/types.js';6import cors from 'cors';7import { log, Actor } from 'apify';8
9// Initialize the Apify Actor environment10// This call configures the Actor for its environment and should be called at startup11await Actor.init();12
13const getServer = () => {14 // Create an MCP server with implementation details15 const server = new McpServer(16 {17 name: 'ts-mcp-empty',18 version: '1.0.0',19 },20 { capabilities: { logging: {} } },21 );22
23 // Register a tool for adding two numbers with structured output24 server.registerTool(25 'add',26 {27 description: 'Adds two numbers together and returns the sum with structured output',28 inputSchema: {29 a: z.number().describe('First number to add'),30 b: z.number().describe('Second number to add'),31 },32 outputSchema: {33 result: z.number().describe('The sum of a and b'),34 operands: z.object({35 a: z.number(),36 b: z.number(),37 }),38 operation: z.string().describe('The operation performed'),39 },40 },41 async ({ a, b }): Promise<CallToolResult> => {42 try {43 // Charge for the tool call44 await Actor.charge({ eventName: 'tool-call' });45 log.info('Charged for tool-call event');46
47 const sum = a + b;48 const structuredContent = {49 result: sum,50 operands: { a, b },51 operation: 'addition',52 };53
54 return {55 content: [56 {57 type: 'text',58 text: `The sum of ${a} and ${b} is ${sum}`,59 },60 ],61 structuredContent,62 };63 } catch (error) {64 log.error('Error in add tool:', {65 error,66 });67 throw error;68 }69 },70 );71
72 // Create a simple dummy resource at a fixed URI73 server.registerResource(74 'calculator-info',75 'https://example.com/calculator',76 { mimeType: 'text/plain' },77 async (): Promise<ReadResourceResult> => {78 return {79 contents: [80 {81 uri: 'https://example.com/calculator',82 text: 'This is a simple calculator MCP server that can add two numbers together.',83 },84 ],85 };86 },87 );88
89 return server;90};91
92const app = express();93app.use(express.json());94
95// Configure CORS to expose Mcp-Session-Id header for browser-based clients96app.use(97 cors({98 origin: '*', // Allow all origins - adjust as needed for production99 exposedHeaders: ['Mcp-Session-Id'],100 }),101);102
103// Readiness probe handler104app.get('/', (req: Request, res: Response) => {105 if (req.headers['x-apify-container-server-readiness-probe']) {106 log.info('Readiness probe');107 res.end('ok\n');108 return;109 }110 res.status(404).end();111});112
113app.post('/mcp', async (req: Request, res: Response) => {114 const server = getServer();115 try {116 const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({117 sessionIdGenerator: undefined,118 });119 await server.connect(transport);120 await transport.handleRequest(req, res, req.body);121 res.on('close', () => {122 log.info('Request closed');123 transport.close();124 server.close();125 });126 } catch (error) {127 log.error('Error handling MCP request:', {128 error,129 });130 if (!res.headersSent) {131 res.status(500).json({132 jsonrpc: '2.0',133 error: {134 code: -32603,135 message: 'Internal server error',136 },137 id: null,138 });139 }140 }141});142
143app.get('/mcp', (_req: Request, res: Response) => {144 log.info('Received GET MCP request');145 res.writeHead(405).end(146 JSON.stringify({147 jsonrpc: '2.0',148 error: {149 code: -32000,150 message: 'Method not allowed.',151 },152 id: null,153 }),154 );155});156
157app.delete('/mcp', (_req: Request, res: Response) => {158 log.info('Received DELETE MCP request');159 res.writeHead(405).end(160 JSON.stringify({161 jsonrpc: '2.0',162 error: {163 code: -32000,164 message: 'Method not allowed.',165 },166 id: null,167 }),168 );169});170
171// Start the server172const PORT = process.env.APIFY_CONTAINER_PORT ? parseInt(process.env.APIFY_CONTAINER_PORT) : 3000;173app.listen(PORT, (error) => {174 if (error) {175 log.error('Failed to start server:', {176 error,177 });178 process.exit(1);179 }180 log.info(`MCP Server listening on port ${PORT}`);181});182
183// Handle server shutdown184process.on('SIGINT', async () => {185 log.info('Shutting down server...');186 process.exit(0);187});MCP server template
A template for creating a Model Context Protocol server using Streamable HTTP transport on Apify platform.
This template includes a simple example MCP server with:
- An
addtool that adds two numbers together with structured output - A dummy
calculator-inforesource endpoint - Pay Per Event monetization support
How to use
- Modify the server: Edit
src/main.tsto add your own tools and resources - Add new tools: Use the
server.registerTool()method to register new tools - Add new resources: Use the
server.registerResource()method to register new resources - Update billing: Configure billing events in
.actor/pay_per_event.jsonand charge for tool calls
The server runs on port 3000 (or APIFY_CONTAINER_PORT if set) and exposes the MCP protocol at the /mcp endpoint.
Running locally
npm installnpm run start:dev
The server will start and listen for MCP requests at http://localhost:3000/mcp
Deploying to Apify
Push your Actor to the Apify platform and configure standby mode.
Then connect to the Actor endpoint with your MCP client: https://me--my-mcp-server.apify.actor/mcp using the Streamable HTTP transport.
Important: When connecting to your deployed MCP server, pass your Apify API token in the Authorization header as a Bearer token:
Authorization: Bearer <YOUR_APIFY_API_TOKEN>
Pay per event
This template uses the Pay Per Event (PPE) monetization model, which provides flexible pricing based on defined events.
To charge users, define events in JSON format and save them on the Apify platform. Here is an example schema with the tool-call event:
[{"tool-call": {"eventTitle": "Price for completing a tool call","eventDescription": "Flat fee for completing a tool call.","eventPriceUsd": 0.05}}]
In the Actor, trigger the event with:
await Actor.charge({ eventName: 'tool-call' });
This approach allows you to programmatically charge users directly from your Actor, covering the costs of execution and related services.
To set up the PPE model for this Actor:
- Configure Pay Per Event: establish the Pay Per Event pricing schema in the Actor's Monetization settings. First, set the Pricing model to
Pay per eventand add the schema. An example schema can be found in .actor/pay_per_event.json.
Resources
Crawlee + Cheerio
A scraper example that uses Cheerio to parse HTML. It's fast, but it can't run the website's JavaScript or pass JS anti-scraping challenges.
One‑Page HTML Scraper with Cheerio
Scrape single page with provided URL with Axios and extract data from page's HTML with Cheerio.
Crawlee + Puppeteer + Chrome
Example of a Puppeteer and headless Chrome web scraper. Headless browsers render JavaScript and are harder to block, but they're slower than plain HTTP.
Crawlee + Playwright + Chrome
Web scraper example with Crawlee, Playwright and headless Chrome. Playwright is more modern, user-friendly and harder to block than Puppeteer.
Crawlee + Playwright + Camoufox
Web scraper example with Crawlee, Playwright and headless Camoufox. Camoufox is a custom stealthy fork of Firefox. Try this template if you're facing anti-scraping challenges.
Playwright + Chrome Test Runner
Example of using the Playwright Test project to run automated website tests in the cloud and display their results. Usable as an API.