Skip to content

TypeScript MCP server

An MCP server that gives AI assistants like Claude and Cursor your own tools, with built-in pay-per-event billing.

Create it locally

$scrapely create my-actor --template ts-mcp-empty

Main file

.TS
src/main.ts
import express, { Request, Response } from 'express';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import * as z from 'zod';
import { CallToolResult, ReadResourceResult } from '@modelcontextprotocol/sdk/types.js';
import cors from 'cors';
import { log, Actor } from 'scrapely';

// Initialize the Scrapely Actor environment
// This call configures the Actor for its environment and should be called at startup
await Actor.init();

const getServer = () => {
    // Create an MCP server with implementation details
    const server = new McpServer(
        {
            name: 'ts-mcp-empty',
            version: '1.0.0',
        },
        { capabilities: { logging: {} } },
    );

    // Register a tool for adding two numbers with structured output
    server.registerTool(
        'add',
        {
            description: 'Adds two numbers together and returns the sum with structured output',
            inputSchema: {
                a: z.number().describe('First number to add'),
                b: z.number().describe('Second number to add'),
            },
            outputSchema: {
                result: z.number().describe('The sum of a and b'),
                operands: z.object({
                    a: z.number(),
                    b: z.number(),
                }),
                operation: z.string().describe('The operation performed'),
            },
        },
        async ({ a, b }): Promise<CallToolResult> => {
            try {
                // Charge for the tool call
                await Actor.charge({ eventName: 'tool-call' });
                log.info('Charged for tool-call event');

                const sum = a + b;
                const structuredContent = {
                    result: sum,
                    operands: { a, b },
                    operation: 'addition',
                };

                return {
                    content: [
                        {
                            type: 'text',
                            text: `The sum of ${a} and ${b} is ${sum}`,
                        },
                    ],
                    structuredContent,
                };
            } catch (error) {
                log.error('Error in add tool:', {
                    error,
                });
                throw error;
            }
        },
    );

    // Create a simple dummy resource at a fixed URI
    server.registerResource(
        'calculator-info',
        'https://example.com/calculator',
        { mimeType: 'text/plain' },
        async (): Promise<ReadResourceResult> => {
            return {
                contents: [
                    {
                        uri: 'https://example.com/calculator',
                        text: 'This is a simple calculator MCP server that can add two numbers together.',
                    },
                ],
            };
        },
    );

    return server;
};

const app = express();
app.use(express.json());

// Configure CORS to expose Mcp-Session-Id header for browser-based clients
app.use(
    cors({
        origin: '*', // Allow all origins - adjust as needed for production
        exposedHeaders: ['Mcp-Session-Id'],
    }),
);

// Readiness probe handler
app.get('/', (req: Request, res: Response) => {
    if (req.headers['x-scrapely-standby-readiness-probe']) {
        log.info('Readiness probe');
        res.end('ok\n');
        return;
    }
    res.status(404).end();
});

app.post('/mcp', async (req: Request, res: Response) => {
    const server = getServer();
    try {
        const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
            sessionIdGenerator: undefined,
        });
        await server.connect(transport);
        await transport.handleRequest(req, res, req.body);
        res.on('close', () => {
            log.info('Request closed');
            transport.close();
            server.close();
        });
    } catch (error) {
        log.error('Error handling MCP request:', {
            error,
        });
        if (!res.headersSent) {
            res.status(500).json({
                jsonrpc: '2.0',
                error: {
                    code: -32603,
                    message: 'Internal server error',
                },
                id: null,
            });
        }
    }
});

app.get('/mcp', (_req: Request, res: Response) => {
    log.info('Received GET MCP request');
    res.writeHead(405).end(
        JSON.stringify({
            jsonrpc: '2.0',
            error: {
                code: -32000,
                message: 'Method not allowed.',
            },
            id: null,
        }),
    );
});

app.delete('/mcp', (_req: Request, res: Response) => {
    log.info('Received DELETE MCP request');
    res.writeHead(405).end(
        JSON.stringify({
            jsonrpc: '2.0',
            error: {
                code: -32000,
                message: 'Method not allowed.',
            },
            id: null,
        }),
    );
});

// Start the server
const PORT = process.env.ACTOR_WEB_SERVER_PORT ? parseInt(process.env.ACTOR_WEB_SERVER_PORT) : 3000;
app.listen(PORT, (error) => {
    if (error) {
        log.error('Failed to start server:', {
            error,
        });
        process.exit(1);
    }
    log.info(`MCP Server listening on port ${PORT}`);
});

// Handle server shutdown
process.on('SIGINT', async () => {
    log.info('Shutting down server...');
    process.exit(0);
});

TypeScript MCP server

An MCP server that gives AI assistants like Claude and Cursor your own tools, with built-in pay-per-event billing.

Included features

  • Scrapely SDK — Actor lifecycle, input, storage and proxies
  • Model Context Protocol — tools for AI assistants
  • Dataset schema — structured results with a preview view (.actor/dataset_schema.json)
  • Standby mode — the Actor stays running and answers HTTP requests at its standby URL

Getting started locally

npm install
npm run start      # run the Actor
npm run build      # compile to dist/

Deploying to Scrapely

Create an Actor from this template in the Scrapely Console (Develop new → template → Git). Scrapely creates a repository in your account, pushes this code, and builds the Actor. Every push to your repository triggers a new build.

G2
4.7/5
Excellent
Trustpilot

The internet is full of data.
Start owning yours.

Scrape.ly gives you the infrastructure, the tooling, and the team to extract, process, and operationalize web data at any scale. Setup takes minutes. The advantage lasts forever.

No credit card. No setup calls.