Turning Internal Legacy APIs into an AI-Understood MCP Server — A Complete Guide to Node.js + Claude Code Connection
Have you ever spent two weeks developing a separate plugin to integrate AI into a legacy ERP system? I am referring to a situation where your internal REST API works perfectly fine, but you have to build a new custom integration every time Claude or GPT needs to read that data directly. The Model Context Protocol (MCP) is a universal standard that solves this problem, offering the most realistic way to place an "AI-understood translation layer" in front of legacy systems. Since Anthropic released it as open source in November 2024, it surpassed 97 million monthly SDK downloads within six months of launch, and OpenAI, Google, and Microsoft have also declared their support. By 2025, it had established itself as a vendor-neutral standard under the Linux Foundation.
By the time you finish this article, "PROD-001 제품 재고 알려줘"—in short, an AI tool for querying the internal ERP system—will be complete. We will examine the entire process, along with the code, of wrapping internal legacy systems with an MCP server using Node.js and the official TypeScript SDK, and connecting them to Claude Code. We will cover core MCP concepts, actual working code, and even the security points that must be taken into account in production.
Key Concepts
What is MCP — "USB-C for AI"
Just as USB-C connects various devices through a single interface, MCP is a universal adapter between AI models and the outside world. Previously, separate integration was required for each AI, such as plugins for Claude and Function Calling for GPT, but MCP unifies this connection method into a single standard.
MCP (Model Context Protocol): An open protocol that standardizes how AI assistants interact with external tools, databases, and systems. It defines the mechanism for an LLM to "discover" and "call" tools in a server-client structure.
There are three core components of the MCP.
| Components | Roles | Examples of In-house Legacy System Utilization |
|---|---|---|
| Tools | Functions called by the LLM. The AI decides when to execute automatically | Legacy ERP API calls, DB query execution |
| Resources | Read-only data sources for LLM | Internal API specifications, technical documentation, wiki |
| Prompts | Reusable Prompt Templates | ERP Query Guide, Code Convention Document |
Communication Method: stdio vs Streamable HTTP
| Method | Use | Features |
|---|---|---|
| stdio | Local Process | Simple Setup, Suitable for Personal Development Environments |
| Streamable HTTP(SSE) | Remote Server | Team-wide Sharing, Enterprise Environment |
This article explains the stdio method, which is the fastest way to get started.
Legacy System Wrapping Pattern
The pattern for wrapping in-house legacy systems with an MCP server mostly involves wrapping existing APIs with a tool (arXiv Empirical Study). Without requiring modifications to the legacy system, the MCP server acts as a translation layer in front of it.
Claude Code ──MCP 프로토콜──▶ MCP 서버 (Node.js) ──HTTP──▶ 레거시 ERP/APIThe reason McpServer and StdioServerTransport were separated
The reason McpServer and StdioServerTransport were separated into distinct objects in the SDK is that they were designed to allow the transport layer and application layer to be replaced independently. If you use stdio for local development and then need to share with a team, you simply need to replace StdioServerTransport with StreamableHTTPServerTransport. You do not need to touch the tool registration code at all.
Practical Application
Implementation of ERP Inventory Lookup Tool
This is the most common scenario. We configure the legacy ERP REST API to be exposed via an MCP tool, enabling Claude Code to query inventory data using natural language.
We start with the initial project setup.
mkdir legacy-erp-mcp && cd legacy-erp-mcp
pnpm init
pnpm add @modelcontextprotocol/sdk zod axios
pnpm add -D typescript @types/node// package.json (핵심 설정)
{
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0",
"zod": "^3.23.0",
"axios": "^1.7.0"
}
}Why "type": "module" is needed: Node.js interprets files in the CommonJS (CJS) style by default. Since the MCP SDK is packaged in the ESM (ES Module) style, an error occurs in the import syntax without this setting. Unlike Python's from module import X, Node.js requires this setting to be explicitly declared.
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
"outDir": "dist"
}
}Now, we will create the MCP server main body.
// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import axios from "axios";
// 환경변수 초기화 시 검증 — TypeScript strict 모드에서는 undefined 가능성을 처리해야 합니다
const ERP_BASE_URL = process.env.ERP_BASE_URL;
const ERP_API_KEY = process.env.ERP_API_KEY;
if (!ERP_BASE_URL || !ERP_API_KEY) {
console.error("필수 환경변수(ERP_BASE_URL, ERP_API_KEY)가 설정되지 않았습니다.");
process.exit(1);
}
const server = new McpServer({
name: "legacy-erp-server",
version: "1.0.0",
});
// Tool 1: 재고 현황 조회
server.tool(
"get_inventory",
"특정 제품 코드(예: PROD-001)로 현재 재고 수량과 위치를 조회합니다",
{
product_code: z.string().describe("제품 코드 (예: PROD-001)"),
},
async ({ product_code }) => {
try {
const res = await axios.get(
`${ERP_BASE_URL}/inventory/${product_code}`,
{ headers: { "X-API-Key": ERP_API_KEY } }
);
return {
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
};
} catch (err) {
// isError: true — Claude가 Tool 실패를 인식하고 대안을 제안할 수 있도록 하는 신호
return {
content: [{ type: "text", text: `오류: ${(err as Error).message}` }],
isError: true,
};
}
}
);
// Tool 2: 발주 목록 조회
server.tool(
"get_purchase_orders",
"특정 기간의 발주 목록을 조회합니다",
{
from_date: z.string().describe("시작일 (YYYY-MM-DD)"),
to_date: z.string().describe("종료일 (YYYY-MM-DD)"),
status: z
.enum(["pending", "approved", "shipped", "completed"])
.optional()
.describe("발주 상태 필터"),
},
async ({ from_date, to_date, status }) => {
try {
const params = { from_date, to_date, ...(status && { status }) };
const res = await axios.get(`${ERP_BASE_URL}/purchase-orders`, {
params,
headers: { "X-API-Key": ERP_API_KEY },
});
return {
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
};
} catch (err) {
return {
content: [{ type: "text", text: `오류: ${(err as Error).message}` }],
isError: true,
};
}
}
);
// top-level await는 ES2022 이상 + Node16 모듈 설정에서 사용 가능합니다
const transport = new StdioServerTransport();
await server.connect(transport);The key points of the code can be summarized as follows.
| Code Element | Description |
|---|---|
McpServer |
MCP server instance. Identified by name and version |
server.tool() |
Register Tool. Pass Name, Description, Zod Schema, Handler in that order |
z.string().describe() |
Used by Claude to understand the meaning of this parameter |
StdioServerTransport |
Communicating JSON-RPC with Claude Code via stdout/stdin |
isError: true |
Tool failure signal. If this flag is present, Claude recognizes the failure and suggests an alternative. |
Caution: On stdio servers, you must use console.error() instead of console.log(). If you write plain text to stdout, the JSON-RPC message will be corrupted and Claude Code will be unable to parse the response.
Claude Code Connection Settings
After building, register it with Claude Code as .mcp.json.
pnpm build// .mcp.json (프로젝트 루트에 위치, Git으로 팀 공유 가능)
{
"mcpServers": {
"legacy-erp": {
"command": "node",
"args": ["./dist/index.js"],
"env": {
"ERP_BASE_URL": "http://internal-erp.company.com/api/v1",
"ERP_API_KEY": "${ERP_API_KEY}"
}
}
}
}${ERP_API_KEY} Syntax: This is the environment variable referencing syntax natively supported by Claude Code. At runtime, Claude Code automatically reads values from OS environment variables (export ERP_API_KEY=... or .env files) and injects them. You do not need to hardcode API keys directly in .mcp.json.
Registration via CLI is also possible.
# 프로젝트 범위로 등록
claude mcp add legacy-erp node ./dist/index.js
# 등록된 서버 목록 확인
claude mcp list.mcp.json vs CLI Registration: .mcp.json can be committed to Git so that the entire team shares the same MCP server settings. If it is a private server, it is recommended to use the CLI's --global option.
Exposure of Internal Document Resources
Resource is a read-only data source that Claude can automatically reference when writing code. The uri parameter is passed as a Node.js URL object, and you can define and use a custom URI scheme like internal-docs://. This scheme is used only to identify resources within the MCP server and is unrelated to actual network requests.
// 사내 기술 문서를 Resource로 노출
server.resource(
"api-spec",
"internal-docs://api-spec", // 커스텀 스킴: MCP 서버 내 식별자로 사용
async (uri) => {
// uri는 URL 객체 — uri.pathname, uri.href 등으로 접근 가능
const content = await fetchFromInternalWiki(uri.pathname);
return {
contents: [
{
uri: uri.href,
mimeType: "text/markdown",
text: content,
},
],
};
}
);
async function fetchFromInternalWiki(path: string): Promise<string> {
const wikiUrl = process.env.WIKI_BASE_URL;
const wikiToken = process.env.WIKI_TOKEN;
const res = await axios.get(`${wikiUrl}${path}`, {
headers: { Authorization: `Bearer ${wikiToken}` },
});
return res.data.content;
}Local Debugging with MCP Inspector
It is convenient to use the official debugging GUI to check if the tool is working properly during development.
npx @modelcontextprotocol/inspector node ./dist/index.jsThe Inspector UI opens in the browser, allowing you to check the list of registered tools or call them directly to test their responses. Going through this step before connecting to Claude Code significantly reduces debugging time.
Pros and Cons Analysis
Advantages
| Item | Content |
|---|---|
| Standardization | Connects to all AI tools such as Claude, GPT, Gemini via the same interface |
| Legacy Protection | Add only the AI access layer without modifying the existing system |
| Rapid Deployment | Prototypes can be completed within hours using the official SDK |
| Team Sharing | .mcp.json Sharing Hanaro Project Unit Settings |
| Isolation Structure | Ensuring safety by restricting AI to calling only verified tools |
Disadvantages and Precautions
| Item | Content | Response Plan |
|---|---|---|
| Secret Exposure | 88% of public MCP servers use long-valid static API keys, posing a high risk of theft (Astrix Security Report) | Use OAuth 2.0 + short-term tokens. Store credentials separately with a secret manager like HashiCorp Vault and inject them at runtime |
| Injection Vulnerability | Command injection vulnerabilities found in 43% of analyzed servers | Strictly validate all inputs using the Zod schema. Recommend a whitelist approach at the z.string().max(50).regex(/^[A-Z0-9-]+$/) level |
| Local-only limitations | stdio servers are inaccessible from anywhere other than the running machine | If team-wide sharing is required, switch to a Streamable HTTP-based remote server |
| Context Cost | Claude context token usage increases as the number of tools increases | Operate servers separately by domain (e.g., erp-server, docs-server) |
| Maintenance Burden | MCP servers must be modified when legacy APIs are changed | Managing legacy API specifications as OpenAPI facilitates change tracking, and tools like Stainless can automatically generate MCP servers |
Zod: A schema validation library for the TypeScript ecosystem. Validation is performed automatically at runtime when input types are declared using z.string(), z.number(), z.enum(), etc. It serves as the first line of defense against SQL injection or command injection in MCP tools.
OAuth Resource Server: With the revision of the MCP specification in June 2025, the MCP server has been officially classified as an OAuth Resource Server. In production environments, OAuth 2.0/OIDC-based short-term token authentication is recommended instead of environment variable API keys. The structure is such that the Authorization Server handles token issuance and renewal, while the MCP server only verifies token validity.
The Most Common Mistakes in Practice
- When using
console.log()for debugging: JSON-RPC parsing fails if you write random text to stdout on the stdio server. It is highly recommended to useconsole.error()or introduce a file logger. - When writing tool descriptions roughly: Claude decides when and which tool to call based on the contents of the tool's
descriptionand the parameter'sdescribe(). If you write it specifically like"특정 제품 코드(예: PROD-001)로 현재 재고 수량과 위치를 조회합니다"rather than"재고 조회", the probability that Claude will select the correct tool increases. - When omitting error handling: If the MCP server crashes when a legacy API returns a timeout or a 500 error, the entire Claude Code session is terminated. If you use try-catch together with
isError: trueas shown in the pattern below, Claude can recognize the failure and suggest an alternative.
// 권장 에러 핸들링 패턴
async ({ product_code }) => {
try {
const res = await axios.get(`${ERP_BASE_URL}/inventory/${product_code}`, {
headers: { "X-API-Key": ERP_API_KEY },
});
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
} catch (err) {
return {
content: [{ type: "text", text: `오류: ${(err as Error).message}` }],
isError: true, // Claude가 Tool 실패를 인식하고 다음 행동을 결정
};
}
}In Conclusion
Legacy systems are a culmination of business logic and data accumulated over many years. MCP is a realistic path to overlay AI interfaces while preserving those assets. The core of the pattern implemented in this article is error handling, including isError: true. No matter how well a tool is built, failures must be communicated correctly so that Claude can understand the situation and proceed to the next step. There is a significant difference in actual usability between a server that swallows errors and one that notifies Claude of them.
3 Steps to Start Right Now:
- Environment Preparation: Initialize the project as
mkdir my-mcp && cd my-mcp && pnpm init && pnpm add @modelcontextprotocol/sdk zod axios, and apply thetsconfig.jsonandpackage.jsonsettings introduced above. - First Tool Registration: It is recommended to select one of the legacy API endpoints most frequently queried internally and wrap it with
server.tool(). If direct local access to the internal API is difficult, you can also test the response format first usinghttps://httpbin.org/anythingbefore replacing it with the actual endpoint. You can then verify that the response is received correctly usingnpx @modelcontextprotocol/inspector node ./dist/index.js. - Connecting Claude Code: Add
.mcp.jsonto the project root and open Claude Code to call the registered tools directly using natural language. You can verify that Claude automatically selects and calls theget_inventorytool in response to a question like "Tell me the stock of product PROD-001."
Next Post: Designing an Enterprise MCP Architecture for Secure Team Sharing by Building a Streamable HTTP-Based Remote MCP Server and Applying OAuth 2.0 Authentication