Designing a Generic TypeScript MCP Server Framework – Lesson 11

This article walks through building a reusable TypeScript MCP Server framework using strategy and factory patterns, showing project setup, core type definitions, a sample DeployHtml tool, a singleton registry, Express‑SSE integration, automatic tool loading, and a side‑by‑side comparison with a Java implementation.

Ubiquitous Tech
Ubiquitous Tech
Ubiquitous Tech
Designing a Generic TypeScript MCP Server Framework – Lesson 11

This tutorial demonstrates how to design a generic MCP Server framework in TypeScript, applying strategy and factory patterns so that business‑logic code stays focused while the framework handles routing, registration, and protocol details.

Project structure

First create a project folder with the following layout:

Configuration files

Define tsconfig.json to target ES2022 and output to dist:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "outDir": "dist", // ✅ output to dist
    "rootDir": "src"
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

Define package.json with dependencies on the MCP SDK, Express, and node‑fetch:

{
  "name": "@wuchubuzai/generic-mcp-server",
  "version": "0.0.1",
  "description": "基于通用框架的MCP Server,集成了多种MCP Server",
  "author": "爱海贼的无处不在",
  "type": "module",
  "license": "MIT",
  "main": "bin/index.js",
  "bin": { "generic-mcp-server": "dist/index.js" },
  "files": ["dist"],
  "scripts": {
    "build": "tsc && shx chmod +x dist/*.js",
    "prepare": "npm run build",
    "watch": "tsc --watch"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "1.15.1",
    "@types/node-fetch": "^2.6.12",
    "express": "^4.18.2",
    "node-fetch": "^3.3.2"
  },
  "devDependencies": {
    "@types/express": "^5.0.1",
    "@types/node": "^22.15.3",
    "shx": "^0.3.4",
    "typescript": "^5.8.3"
  },
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" }
}

Core type definitions

In src/tool/types.ts define a response class and the tool contract:

import { Tool } from "@modelcontextprotocol/sdk/types.js";
/**
 * Tool execution result – similar to a Java Response object
 */
export class ToolResponse {
  public content: Array<{ type: "text"; text: string }>;
  public isError: boolean;
  constructor(isError: boolean, text: string) {
    this.isError = isError;
    this.content = [{ type: "text", text }];
  }
  public static success(text: string): ToolResponse {
    return new ToolResponse(false, text);
  }
  public static error(text: string): ToolResponse {
    return new ToolResponse(true, text);
  }
}
/**
 * Interface that every MCP tool must implement – analogous to a Java interface
 */
export interface ToolModule {
  /** MCP‑exposed tool metadata */
  getDefinition(): Tool;
  /** Execute the tool logic – similar to Java's execute(Map<String, Object> args) */
  execute(args: Record<string, any>): Promise<ToolResponse>;
}

Sample tool implementation

The DeployHtmlTool publishes an HTML string to a static‑page service:

import fetch from "node-fetch";
import { ToolModule, ToolResponse } from "./types.js";
import { Tool } from "@modelcontextprotocol/sdk/types.js";
/**
 * DeployHtmlTool – implements the strategy pattern
 * - Definition: tool metadata
 * - Execution: calls remote service to publish HTML
 */
export class DeployHtmlTool implements ToolModule {
  private static readonly definition: Tool = {
    name: "deploy_html",
    description: "将完整 HTML 发布到 Pages,并返回访问 URL。",
    inputSchema: {
      type: "object",
      properties: { value: { type: "string", description: "HTML 字符串" } },
      required: ["value"]
    }
  };
  public getDefinition(): Tool { return DeployHtmlTool.definition; }
  public async execute(args: Record<string, any>): Promise<ToolResponse> {
    const html = args.value;
    if (!html || typeof html !== "string") {
      return ToolResponse.error("参数错误:请提供非空的 HTML 内容。");
    }
    try {
      const res = await fetch("http://localhost:9001/html/generate", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ content: html })
      });
      if (!res.ok) {
        return ToolResponse.error(`[deploy_html] HTTP ${res.status}`);
      }
      const body: any = await res.json();
      return ToolResponse.success(body.data);
    } catch (e: unknown) {
      const msg = e instanceof Error ? e.message : String(e);
      return ToolResponse.error(`[deploy_html] 异常:${msg}`);
    }
  }
}

Tool registry (factory)

The singleton ToolRegistry holds all tool instances and exposes lookup methods:

import { Tool } from "@modelcontextprotocol/sdk/types.js";
import { ToolModule } from "./types.js";
import { DeployHtmlTool } from "./DeployHtmlTool.js";
/**
 * ToolRegistry – equivalent to a Java static map
 */
export class ToolRegistry {
  private static registry: Map<string, ToolModule> = new Map();
  /** Register all tools at application start */
  public static init(): void {
    const deployHtml = new DeployHtmlTool();
    ToolRegistry.registry.set(deployHtml.getDefinition().name, deployHtml);
  }
  /** Retrieve a tool by name */
  public static get(name: string): ToolModule | undefined {
    return ToolRegistry.registry.get(name);
  }
  /** List all tool definitions */
  public static listDefinitions(): Tool[] {
    return Array.from(ToolRegistry.registry.values()).map(mod => mod.getDefinition());
  }
}

Main server entry

src/index.ts

wires Express, the SSE transport, and the MCP SDK handlers:

#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
import { CallToolRequestSchema, ListToolsRequestSchema, Tool } from "@modelcontextprotocol/sdk/types.js";
import { ToolRegistry } from "./tools/ToolRegistry.js";
interface CallToolRequestParams { name: string; arguments: Record<string, any>; }
async function main() {
  // 1. Initialise the tool registry
  ToolRegistry.init();
  // 2. Basic Express + SSE setup
  const app = express();
  app.use(express.json());
  const transports: Record<string, any> = { sse: {} };
  // 3. List tools endpoint
  function handleListTools() { return { tools: ToolRegistry.listDefinitions() }; }
  // 4. Call tool endpoint
  async function handleCallTool(req: any) {
    const params = req.params as CallToolRequestParams;
    const module = ToolRegistry.get(params.name);
    if (!module) {
      return { content: [{ type: "text", text: `Unknown tool: ${params.name}` }], isError: true };
    }
    return module.execute(params.arguments);
  }
  // 5. SSE connection entry
  app.get("/sse", async (req, res) => {
    console.log("http /sse request start.......");
    const transport = new SSEServerTransport('/messages', res);
    transports.sse[transport.sessionId] = transport;
    res.on("close", () => delete transports.sse[transport.sessionId]);
    const server = new Server({ name: "custom-pages-mcp-server", version: "0.0.1" }, { capabilities: { tools: {} } });
    server.setRequestHandler(ListToolsRequestSchema, handleListTools);
    server.setRequestHandler(CallToolRequestSchema, handleCallTool);
    await server.connect(transport);
    console.log("http /sse request connect");
  });
  // 6. SSE message push endpoint
  app.post("/messages", async (req, res) => {
    console.log("http /sse request receive message");
    const sessionId = String(req.query.sessionId);
    const transport = transports.sse[sessionId];
    if (!transport) return res.status(400).send("Invalid sessionId");
    await transport.handlePostMessage(req, res, req.body);
  });
  // 7. Start HTTP service
  const PORT = process.env.PORT || 3000;
  app.listen(PORT, () => console.log(`MCP Server running at http://localhost:${PORT}`));
}
main().catch(e => console.error("Fatal error:", e));

Build and run

Compile and start the server:

npm run build
node dist/index.js

After the server starts, add a configuration in the MCP client and test the deploy_html tool. Screenshots of the client interaction and the resulting web page are shown below:

Automatic tool loading

To avoid manual registration, an index.ts inside the tools folder re‑exports all tool modules, and a factory class scans the folder at runtime. The directory layout changes accordingly (see images). This mirrors Java's SPI mechanism.

Java ↔ TypeScript comparison

Interface – ToolModule defines the contract, similar to a Java interface.

Class – DeployHtmlTool implements business logic.

Singleton registry – ToolRegistry manages all tools, analogous to a static Java map.

Main class – the main function wires the framework and routing.

With this generic framework in place, future TypeScript MCP Server projects can focus on implementing new tools without rewriting boilerplate code.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

backendStrategy PatternTypeScriptNode.jsFactory PatternExpressMCP ServerServer Framework
Ubiquitous Tech
Written by

Ubiquitous Tech

A ubiquitous public account for pirate enthusiasts, regularly sharing curated experiences, tech learning, and growth insights. Currently publishing articles on AI RAG customer service, AI MCP technology, and open-source design. Personal free Knowledge Planet: Awakening New World Programmer.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.