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.
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.tswires 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.jsAfter 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
