Connecting Internal APIs to Claude Code: How to Implement an MCP Server with TypeScript
As AI-assisted development tools became an integral part of the team's workflow, the question of "how to connect LLM with our internal systems" became a practical task rather than a choice. Previously, connecting internal REST APIs to models like Claude required writing separate custom integration codes. This meant rebuilding from scratch every time the client changed, resulting in a recurring situation where integration code for Claude, Copilot, and Cursor accumulated separately for the same API.
The Model Context Protocol (MCP), released by Anthropic in 2024, solves this problem in a standardized way. Currently, major AI development tools such as Claude Code, Cursor, VS Code Copilot, Zed, and Replit support MCP, allowing a server to be reused identically across all clients once implemented. In this article, we will examine the step-by-step process of extending an existing MCP server using the TypeScript SDK and connecting internal APIs to Claude Code. It covers practical content that can be immediately applied in the workplace, ranging from error handling and security precautions to team deployment settings.
This article is intended for developers familiar with TypeScript and REST APIs. Even if you are new to MCP, you can follow along as we explain the core concepts step-by-step. However, this article only covers the stdio transport for local environments. Streamable HTTP transports for multi-user environments and OAuth 2.1 authentication integration will be covered in the next article.
Key Concepts
What is MCP — "USB-C Port for AI"
Model Context Protocol is an open standard that unifies how AI models interact with external tools and data sources. Just as a USB-C port connects various devices under a single standard, MCP serves as a standard interface between AI clients and external systems.
MCP (Model Context Protocol): An open standard released by Anthropic in 2024. It aims to eliminate client-specific custom integration code by standardizing how LLMs interact with external APIs, databases, and tools.
The architecture consists of three layers.
| Role | Component | Example |
|---|---|---|
| Host | AI Client | Claude Code, Claude Desktop |
| Client | MCP Connector inside the Host | Responsible for protocol negotiation and message routing |
| Server | Implemented directly by the developer | Custom server wrapping internal APIs |
Three primitives provided by the MCP server
The MCP server can expose the following three types of functions to Claude.
| Primitive | Role | Practical Examples |
|---|---|---|
| Tools | Functions Claude can call (Actions, Calculations, External Requests) | Calling Internal REST API Endpoints |
| Resources | Read-only data sources | Internal wiki, DB schema, document |
| Prompts | Reusable Prompt Templates | Code Review Instruction Templates |
Transport Methods — Local vs. Remote
There are two ways for an MCP server to communicate with clients. The deployment strategy varies significantly depending on the method selected.
| Transport | Suitable Environment | Features |
|---|---|---|
| StdioServerTransport | Local development, personal machine | No separate server required, simple setup |
| StreamableHTTPServerTransport | Production, Multi-user, Cloud | Replacing SSE with specification revision in March 2025 |
Streamable HTTP: This is the transmission method that officially replaced the existing Server-Sent Events (SSE) method following the revision of the MCP specification on March 26, 2025. According to industry estimates, approximately 70% of current production deployments use Streamable HTTP, so it is recommended to use it as the default for new projects.
Practical Application
First, configure the development environment.
pnpm init
pnpm add @modelcontextprotocol/sdk zod
pnpm add -D typescript tsx @types/nodeTo build a TypeScript project correctly, tsconfig.json is also required.
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "dist",
"strict": true,
"esModuleInterop": true
},
"include": ["src"]
}And add ESM settings and build scripts to package.json.
{
"type": "module",
"scripts": {
"dev": "tsx src/server.ts",
"build": "tsc",
"start": "node dist/server.js"
}
}The "type": "module" setting is required to correctly interpret ESM imports using the .js extension (e.g., ...stdio.js).
Example 1: Wrapping Internal Employee Information API with MCP Tool
Below is a basic server implementation that exposes internal employee information REST APIs as MCP Tools and Resources.
// src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "company-api-server",
version: "1.0.0",
});
// 사내 직원 정보 API를 Tool로 노출
server.tool(
"get-employee-info",
"직원 정보를 사번으로 조회합니다",
{
employeeId: z.string().describe("직원 사번 (예: EMP-1234)"),
},
async ({ employeeId }) => {
try {
const response = await fetch(
`https://internal-api.company.com/employees/${employeeId}`,
{
headers: {
Authorization: `Bearer ${process.env.INTERNAL_API_TOKEN}`,
},
}
);
if (!response.ok) {
return {
content: [
{
type: "text",
text: `오류: 직원 정보를 가져오지 못했습니다 (상태 코드: ${response.status})`,
},
],
isError: true,
};
}
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `네트워크 오류: ${error instanceof Error ? error.message : "알 수 없는 오류"}`,
},
],
isError: true,
};
}
}
);
// 사내 프로젝트 목록을 Resource로 노출
server.resource(
"project-list",
"internal://projects/active",
{ mimeType: "application/json" },
async () => {
const response = await fetch(
"https://internal-api.company.com/projects?status=active",
{
headers: {
Authorization: `Bearer ${process.env.INTERNAL_API_TOKEN}`,
},
}
);
const data = await response.json();
return {
contents: [
{
uri: "internal://projects/active",
mimeType: "application/json",
text: JSON.stringify(data, null, 2),
},
],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);Let's go over a few key points.
- If you pass the Zod schema as the third argument to
server.tool(), the input value is automatically validated at runtime. z.string().describe()The text serves as a hint for Claude to understand the purpose of the parameters. The more sufficient the explanation, the more accurately Claude utilizes the Tool.- Returning
isError: trueallows Claude to explicitly recognize that the Tool execution failed. If only error text is returned, Claude may not properly recognize the failure situation. internal://projects/activeis a custom URI scheme used only within this MCP server. It is not resolved directly by clients from the outside, but rather serves as a namespace to identify resources within the server. Unlike the standard URI scheme (https://, file://), it directly defines how the MCP server handles the URI.- It is recommended to handle exceptions such as network failures or JSON parsing failures with
try/catch.
Zod: A runtime schema validation library for the TypeScript ecosystem. When used with the MCP SDK, it can guarantee the types of Tool parameters at both compile time and runtime.
Example 2: Actually Extending an Existing Open Source MCP Server
This is a pattern used when you want to add in-house specialized tools instead of using a public MCP server (e.g., PostgreSQL MCP, GitHub MCP) as is. The key is to separate the existing server into a factory function and register the additional tools to the returned instance.
First, expose the base server as a factory function (assuming you forked an open source server).
// src/base-server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export function createBaseServer(): McpServer {
const server = new McpServer({
name: "base-server",
version: "1.0.0",
});
// 오픈소스 서버가 제공하는 기본 Tool
server.tool(
"query-database",
"내부 데이터베이스를 조회합니다",
{ table: z.string().describe("조회할 테이블 이름") },
async ({ table }) => {
// ... DB 조회 로직
return { content: [{ type: "text", text: `${table} 조회 결과` }] };
}
);
return server; // 인스턴스를 반환해 외부에서 확장 가능하게 합니다
}Now, bring in the base server and register additional in-house specialized tools.
// src/extended-server.ts
import { createBaseServer } from "./base-server.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// 기존 서버 인스턴스를 가져옵니다
const server = createBaseServer();
// 사내 기술 문서 검색 Tool을 추가 등록합니다
server.tool(
"search-internal-docs",
"사내 기술 문서를 키워드로 검색합니다",
{
query: z.string().describe("검색 키워드"),
category: z
.enum(["architecture", "runbook", "api-spec", "onboarding"])
.optional()
.describe("문서 카테고리 필터"),
},
async ({ query, category }) => {
const params = new URLSearchParams({ q: query });
if (category) params.set("category", category);
try {
const response = await fetch(
`https://docs.internal.company.com/api/search?${params}`,
{
headers: { Authorization: `Bearer ${process.env.DOCS_API_TOKEN}` },
}
);
if (!response.ok) {
return {
content: [
{
type: "text",
text: `오류: 문서 검색 실패 (상태 코드: ${response.status})`,
},
],
isError: true,
};
}
const data = await response.json() as {
items: Array<{ title: string; url: string; summary: string }>;
};
// 실제 프로덕션 코드에서는 Zod로 API 응답 타입을 검증하는 것을 권장합니다
return {
content: [
{
type: "text",
text: data.items
.map((item) => `## ${item.title}\n${item.url}\n${item.summary}`)
.join("\n\n"),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `네트워크 오류: ${error instanceof Error ? error.message : "알 수 없는 오류"}`,
},
],
isError: true,
};
}
}
);
const transport = new StdioServerTransport();
await server.connect(transport);The key to this pattern is that createBaseServer() returns a server instance, and the caller registers it as server.tool(). Since you do not have to modify the open source server directly, merging upstream updates becomes much easier.
Example 3: Debugging the Server with MCP Inspector
It is recommended to use MCP Inspector to verify that the implemented server is functioning correctly.
# 개발 중인 서버를 tsx로 직접 실행하면서 Inspector 연결
npx @modelcontextprotocol/inspector npx tsx src/extended-server.tsRunning the Inspector opens a browser-based GUI, allowing you to perform the following tasks without Claude Code.
- Check the list of registered tools and resources
- Test calling the tool by directly entering parameters
- Verification of response results and error messages
If you want to check with the built file, you can use npx @modelcontextprotocol/inspector node dist/extended-server.js. During development, the tsx version is convenient as it does not require building every time.
Example 4: Deploy to the entire team as .mcp.json
To share the implemented server with the entire team, add the .mcp.json file to the project root and include it in version control.
{
"mcpServers": {
"company-api": {
"command": "node",
"args": ["dist/extended-server.js"],
"env": {
"INTERNAL_API_TOKEN": "${INTERNAL_API_TOKEN}",
"DOCS_API_TOKEN": "${DOCS_API_TOKEN}"
}
}
}
}During development, it is convenient to set it up to run directly as tsx without building every time.
{
"mcpServers": {
"company-api-dev": {
"command": "npx",
"args": ["tsx", "src/extended-server.ts"],
"env": {
"INTERNAL_API_TOKEN": "${INTERNAL_API_TOKEN}",
"DOCS_API_TOKEN": "${DOCS_API_TOKEN}"
}
}
}
}The scope priority of the configuration file is applied in the order of 로컬 > 프로젝트 > 유저. Team shared settings can be managed by placing them in the project scope (.mcp.json), and personal overrides in the local scope.
Pros and Cons Analysis
Advantages
| Item | Content |
|---|---|
| Standardized Reusability | An MCP server implemented once can be used identically on all MCP-compatible clients such as Claude Code, Cursor, VS Code Copilot, Zed, etc. |
| Loose Coupling | AI clients and internal APIs can be updated independently, so changes to one do not affect the other |
| Zod-based Type Safety | Automatically validates input parameters at runtime to block invalid values before they reach internal APIs |
| Gradual Adoption | Low adoption risk as you can expose existing REST APIs to Claude simply by wrapping them without modifying them |
| Team Sharing | Committing .mcp.json to the repository allows the entire team to share the same MCP settings |
Disadvantages and Precautions
| Item | Major Risks | Response Measures |
|---|---|---|
| Dependence on Static Secrets | 53% of MCP servers rely on long-life API keys, posing a high risk of theft (Astrix Security, 2025) | Use of short-term tokens + AWS Secrets Manager / Vault |
| Prompt Injection | Malicious instructions can be injected into tools that retrieve external content | Responses from untrusted external sources must be sanitized before returning |
| Stdio's Local Limitations | Stdio Transport operates only in local environments | Switch to StreamableHTTPServerTransport in multi-user and cloud environments |
| Speed of Spec Change | Specs evolve rapidly like SSE → Streamable HTTP | Regularly check SDK versions and clearly lock the version range to package.json |
| Low OAuth Adoption Rate | The OAuth adoption rate for MCP servers is only 8.5% (Astrix Security, 2025) | In enterprise environments, the adoption of OAuth 2.1 through Okta and Azure AD integration is set as a long-term goal |
OAuth 2.1: This is a standard authentication protocol used in enterprise environments. With the revision of the MCP specification in June 2025, the MCP server was classified as an official OAuth Resource Server. It integrates by having an Enterprise Identity Management Service (IdP), such as Okta or Azure AD, handle authentication, while the MCP server verifies the issued token. This is not covered in this article and will be explained in detail in the next post.
The Most Common Mistakes in Practice
- When directly hardcoding a secret in
.mcp.json— If you write the actual key value to a file in the form of"INTERNAL_API_TOKEN": "sk-1234...", the secret will be exposed to the storage. It is strongly recommended to use an environment variable reference in the form of"${INTERNAL_API_TOKEN}"and to inject the actual value from the.envfile or the secret management service. - Minor syntax errors in JSON configuration files — Syntax errors in
.mcp.json, such as the use of trailing commas or single quotes, cause the MCP server to fail silently without an error message. It is recommended to enable JSON linting in the editor or use a JSON format validation tool. - If the Tool description is written carelessly — Claude determines when and which Tool to use by looking at the text of the Tool's
nameanddescription, and the parameter's.describe(). If the description is inaccurate or too short, Claude may not be able to select the Tool correctly or may not use it at all.
In Conclusion
MCP is a standard layer that allows you to implement your in-house API once and reuse it across all AI development tools, and using the TypeScript SDK, you can establish the first connection with just dozens of lines of code.
The 3 steps you can start right now are as follows.
- Configuring Development Environment and Registering the First Tool — After installing the dependencies with
pnpm add @modelcontextprotocol/sdk zod, save the code from Example 1 assrc/server.tsto register one of your most frequently used internal API endpoints as a Tool. - Verify operation with MCP Inspector — Run
npx @modelcontextprotocol/inspector npx tsx src/server.tsto call the tool directly from the browser and check if the server responds correctly without Claude Code. - Create
.mcp.jsonand share with the team — Create.mcp.jsonin the project root and commit it to the repository so that all team members can immediately use the same MCP server in Claude Code. It is recommended to separate secrets into the form${ENV_VAR}.
Next Post: This post covers how to deploy an MCP server in a multi-user environment using Streamable HTTP Transport and integrate enterprise authentication with OAuth 2.1. I plan to add a link to this post once the release date is determined.
Reference Materials
- modelcontextprotocol/typescript-sdk | GitHub
- MCP TypeScript SDK Official Documentation | ts.sdk.modelcontextprotocol.io
- Connect Claude Code to tools via MCP | Claude Code 공식 문서
- Build MCP Server with TypeScript: Complete Tutorial (2026) | MCPize
- How to Build a Custom MCP Server with TypeScript | freeCodeCamp
- Getting Started with Claude Desktop and custom MCP servers | WorkOS Blog
- Building your first MCP server | GitHub Blog
- Elastic MCP server: How to create an MCP server with TypeScript | Elastic
- Internal API MCP Server | Enterprise MCP Documentation
- State of MCP Server Security 2025 | Astrix Security
- MCP Server Best Practices for 2026 | CData
- Example Servers | Model Context Protocol Official Site