여러 MCP 서버를 조합하고 클라이언트 애플리케이션을 구축하여, 프로덕션 수준의 풀스택 MCP 시스템을 완성하는 실전 프로젝트입니다.
이 장에서는 시리즈 전체에서 학습한 내용을 종합하여 DevOps 어시스턴트를 구축합니다. 이 시스템은 개발팀이 자연어로 인프라를 조회하고, 배포 상태를 확인하며, 로그를 분석할 수 있는 AI 기반 도구입니다.
세 개의 MCP 서버를 구축하고, 하나의 CLI 클라이언트 애플리케이션으로 통합합니다.
devops-assistant/
servers/
git-server/
src/index.ts
package.json
log-server/
src/index.ts
package.json
system-server/
src/index.ts
package.json
client/
src/
index.ts
mcp-manager.ts
agent.ts
package.json
package.json # 워크스페이스 루트
Git 저장소의 정보를 조회하는 MCP 서버입니다. 읽기 전용 작업만 제공하여 안전성을 보장합니다.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { execFile } from "child_process";
import { promisify } from "util";
const execFileAsync = promisify(execFile);
const server = new McpServer({
name: "git-server",
version: "1.0.0",
description: "Git 저장소 정보를 조회하는 MCP 서버",
});
const REPO_PATH = process.env.REPO_PATH || process.cwd();
// 리소스: 저장소 기본 정보
server.resource(
"repo-info",
"git://info",
{ description: "Git 저장소 기본 정보" },
async () => {
const [branch, remoteUrl, lastCommit] = await Promise.all([
execFileAsync("git", ["-C", REPO_PATH, "branch", "--show-current"]),
execFileAsync("git", ["-C", REPO_PATH, "remote", "get-url", "origin"]).catch(() => ({ stdout: "없음" })),
execFileAsync("git", ["-C", REPO_PATH, "log", "-1", "--format=%H|%an|%ad|%s", "--date=iso"]),
]);
const [hash, author, date, ...msgParts] = lastCommit.stdout.trim().split("|");
const info = {
currentBranch: branch.stdout.trim(),
remoteUrl: remoteUrl.stdout.trim(),
lastCommit: {
hash: hash,
author: author,
date: date,
message: msgParts.join("|"),
},
repoPath: REPO_PATH,
};
return {
contents: [{
uri: "git://info",
text: JSON.stringify(info, null, 2),
mimeType: "application/json",
}],
};
}
);
// 도구: 커밋 히스토리
server.tool(
"git_log",
"Git 커밋 히스토리를 조회합니다. 최근 커밋부터 시간 역순으로 반환합니다.",
{
count: z.number().min(1).max(50).default(10).describe("조회할 커밋 수"),
author: z.string().optional().describe("특정 작성자의 커밋만 필터링"),
since: z.string().optional().describe("시작 날짜 (YYYY-MM-DD 형식)"),
path: z.string().optional().describe("특정 파일/디렉토리의 변경만 조회"),
},
async (params) => {
const args = ["-C", REPO_PATH, "log", "--format=%H|%an|%ad|%s", "--date=short"];
args.push("-" + params.count);
if (params.author) {
args.push("--author=" + params.author);
}
if (params.since) {
args.push("--since=" + params.since);
}
if (params.path) {
args.push("--", params.path);
}
const { stdout } = await execFileAsync("git", args);
const commits = stdout.trim().split("\n").filter(Boolean).map((line) => {
const [hash, author, date, ...msgParts] = line.split("|");
return {
hash: hash.substring(0, 8),
author: author,
date: date,
message: msgParts.join("|"),
};
});
return {
content: [{
type: "text" as const,
text: JSON.stringify(commits, null, 2),
}],
};
}
);
// 도구: 변경된 파일 목록
server.tool(
"git_diff_stat",
"두 커밋 사이에서 변경된 파일 목록과 통계를 조회합니다.",
{
from: z.string().default("HEAD~1").describe("시작 커밋 (기본: HEAD~1)"),
to: z.string().default("HEAD").describe("끝 커밋 (기본: HEAD)"),
},
async (params) => {
const { stdout } = await execFileAsync("git", [
"-C", REPO_PATH, "diff", "--stat", params.from, params.to,
]);
return {
content: [{ type: "text" as const, text: stdout }],
};
}
);
// 도구: 브랜치 목록
server.tool(
"git_branches",
"모든 브랜치 목록을 조회합니다. 현재 브랜치를 표시합니다.",
{},
async () => {
const { stdout } = await execFileAsync("git", [
"-C", REPO_PATH, "branch", "-a", "--format=%(refname:short)|%(committerdate:short)|%(subject)",
]);
const branches = stdout.trim().split("\n").filter(Boolean).map((line) => {
const [name, date, subject] = line.split("|");
return { name: name, lastCommitDate: date, lastCommitMessage: subject };
});
return {
content: [{
type: "text" as const,
text: JSON.stringify(branches, null, 2),
}],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);애플리케이션 로그 파일을 분석하는 MCP 서버입니다.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import fs from "fs/promises";
import path from "path";
const server = new McpServer({
name: "log-server",
version: "1.0.0",
description: "애플리케이션 로그를 분석하는 MCP 서버",
});
const LOG_DIR = process.env.LOG_DIR || "/var/log/app";
// 리소스: 사용 가능한 로그 파일 목록
server.resource(
"log-files",
"logs://files",
{ description: "분석 가능한 로그 파일 목록" },
async () => {
const files = await fs.readdir(LOG_DIR);
const logFiles = [];
for (const file of files.filter((f) => f.endsWith(".log"))) {
const stat = await fs.stat(path.join(LOG_DIR, file));
logFiles.push({
name: file,
size: stat.size,
sizeMB: (stat.size / (1024 * 1024)).toFixed(2) + " MB",
lastModified: stat.mtime.toISOString(),
});
}
return {
contents: [{
uri: "logs://files",
text: JSON.stringify(logFiles, null, 2),
mimeType: "application/json",
}],
};
}
);
// 도구: 로그 검색
server.tool(
"search_logs",
"로그 파일에서 특정 패턴을 검색합니다. 정규식을 지원합니다.",
{
file: z.string().describe("로그 파일 이름"),
pattern: z.string().describe("검색 패턴 (정규식 지원)"),
limit: z.number().default(50).describe("최대 결과 수"),
level: z.enum(["ALL", "ERROR", "WARN", "INFO", "DEBUG"]).default("ALL")
.describe("로그 레벨 필터"),
},
async (params) => {
const filePath = path.join(LOG_DIR, path.basename(params.file));
try {
await fs.access(filePath);
} catch {
return {
content: [{ type: "text" as const, text: "로그 파일을 찾을 수 없습니다: " + params.file }],
isError: true,
};
}
const content = await fs.readFile(filePath, "utf-8");
const lines = content.split("\n");
const regex = new RegExp(params.pattern, "i");
const matches: string[] = [];
for (const line of lines) {
if (params.level !== "ALL" && !line.includes("[" + params.level + "]")) {
continue;
}
if (regex.test(line)) {
matches.push(line);
if (matches.length >= params.limit) break;
}
}
return {
content: [{
type: "text" as const,
text: matches.length > 0
? matches.length + "건의 결과:\n\n" + matches.join("\n")
: "일치하는 로그가 없습니다.",
}],
};
}
);
// 도구: 에러 집계
server.tool(
"error_summary",
"로그 파일의 에러를 집계하여 빈도순으로 정렬합니다.",
{
file: z.string().describe("로그 파일 이름"),
hours: z.number().default(24).describe("최근 N시간 내의 에러만 집계"),
},
async (params) => {
const filePath = path.join(LOG_DIR, path.basename(params.file));
const content = await fs.readFile(filePath, "utf-8");
const lines = content.split("\n");
const cutoff = new Date();
cutoff.setHours(cutoff.getHours() - params.hours);
const errorCounts: Map<string, number> = new Map();
let totalErrors = 0;
for (const line of lines) {
if (!line.includes("[ERROR]")) continue;
// 타임스탬프 파싱 시도
const timestampMatch = line.match(/\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}/);
if (timestampMatch) {
const lineDate = new Date(timestampMatch[0]);
if (lineDate < cutoff) continue;
}
// 에러 메시지 추출 (타임스탬프와 레벨 제거 후)
const errorMsg = line.replace(/.*\[ERROR\]\s*/, "").substring(0, 100);
errorCounts.set(errorMsg, (errorCounts.get(errorMsg) || 0) + 1);
totalErrors++;
}
const sorted = Array.from(errorCounts.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 20)
.map(([message, count]) => ({ message, count }));
const summary = {
totalErrors: totalErrors,
uniqueErrors: errorCounts.size,
period: "최근 " + params.hours + "시간",
topErrors: sorted,
};
return {
content: [{
type: "text" as const,
text: JSON.stringify(summary, null, 2),
}],
};
}
);
// 도구: 최근 로그
server.tool(
"recent_logs",
"로그 파일의 마지막 N줄을 반환합니다.",
{
file: z.string().describe("로그 파일 이름"),
lines: z.number().default(50).describe("반환할 줄 수"),
},
async (params) => {
const filePath = path.join(LOG_DIR, path.basename(params.file));
const content = await fs.readFile(filePath, "utf-8");
const allLines = content.split("\n");
const lastLines = allLines.slice(-params.lines).join("\n");
return {
content: [{ type: "text" as const, text: lastLines }],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);시스템 리소스 사용량을 조회하는 MCP 서버입니다.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import os from "os";
import { execFile } from "child_process";
import { promisify } from "util";
const execFileAsync = promisify(execFile);
const server = new McpServer({
name: "system-server",
version: "1.0.0",
description: "시스템 리소스 모니터링 MCP 서버",
});
// 리소스: 시스템 기본 정보
server.resource(
"system-info",
"system://info",
{ description: "운영체제, CPU, 메모리 등 시스템 기본 정보" },
async () => {
const info = {
hostname: os.hostname(),
platform: os.platform(),
arch: os.arch(),
release: os.release(),
uptime: formatUptime(os.uptime()),
cpus: os.cpus().length + " cores (" + os.cpus()[0]?.model + ")",
totalMemory: formatBytes(os.totalmem()),
};
return {
contents: [{
uri: "system://info",
text: JSON.stringify(info, null, 2),
mimeType: "application/json",
}],
};
}
);
// 도구: CPU 및 메모리 사용량
server.tool(
"get_resource_usage",
"현재 CPU 및 메모리 사용량을 조회합니다.",
{},
async () => {
const totalMem = os.totalmem();
const freeMem = os.freemem();
const usedMem = totalMem - freeMem;
const cpus = os.cpus();
const cpuUsage = cpus.map((cpu) => {
const total = Object.values(cpu.times).reduce((a, b) => a + b, 0);
const idle = cpu.times.idle;
return ((total - idle) / total) * 100;
});
const avgCpuUsage = cpuUsage.reduce((a, b) => a + b, 0) / cpuUsage.length;
const loadAvg = os.loadavg();
const usage = {
cpu: {
usagePercent: avgCpuUsage.toFixed(1) + "%",
loadAverage: {
"1min": loadAvg[0].toFixed(2),
"5min": loadAvg[1].toFixed(2),
"15min": loadAvg[2].toFixed(2),
},
cores: cpus.length,
},
memory: {
total: formatBytes(totalMem),
used: formatBytes(usedMem),
free: formatBytes(freeMem),
usagePercent: ((usedMem / totalMem) * 100).toFixed(1) + "%",
},
};
return {
content: [{
type: "text" as const,
text: JSON.stringify(usage, null, 2),
}],
};
}
);
// 도구: 디스크 사용량
server.tool(
"get_disk_usage",
"디스크 파티션별 사용량을 조회합니다.",
{},
async () => {
try {
const { stdout } = await execFileAsync("df", ["-h", "--output=source,size,used,avail,pcent,target"]);
return {
content: [{ type: "text" as const, text: stdout }],
};
} catch {
// macOS 호환
const { stdout } = await execFileAsync("df", ["-h"]);
return {
content: [{ type: "text" as const, text: stdout }],
};
}
}
);
// 도구: 프로세스 목록
server.tool(
"get_top_processes",
"CPU 또는 메모리 사용량이 높은 프로세스를 조회합니다.",
{
sortBy: z.enum(["cpu", "memory"]).default("cpu").describe("정렬 기준"),
count: z.number().default(10).describe("표시할 프로세스 수"),
},
async (params) => {
const sortFlag = params.sortBy === "cpu" ? "-pcpu" : "-pmem";
try {
const { stdout } = await execFileAsync("ps", [
"aux", "--sort=" + sortFlag,
]);
const lines = stdout.split("\n");
const header = lines[0];
const processes = lines.slice(1, params.count + 1).join("\n");
return {
content: [{
type: "text" as const,
text: header + "\n" + processes,
}],
};
} catch {
// macOS 호환
const { stdout } = await execFileAsync("ps", ["aux"]);
const lines = stdout.split("\n");
const header = lines[0];
const sortIdx = params.sortBy === "cpu" ? 2 : 3;
const sorted = lines.slice(1)
.filter(Boolean)
.sort((a, b) => {
const valA = parseFloat(a.split(/\s+/)[sortIdx] || "0");
const valB = parseFloat(b.split(/\s+/)[sortIdx] || "0");
return valB - valA;
})
.slice(0, params.count);
return {
content: [{
type: "text" as const,
text: header + "\n" + sorted.join("\n"),
}],
};
}
}
);
function formatBytes(bytes: number): string {
const units = ["B", "KB", "MB", "GB", "TB"];
let unitIndex = 0;
let value = bytes;
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024;
unitIndex++;
}
return value.toFixed(1) + " " + units[unitIndex];
}
function formatUptime(seconds: number): string {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const mins = Math.floor((seconds % 3600) / 60);
return days + "일 " + hours + "시간 " + mins + "분";
}
const transport = new StdioServerTransport();
await server.connect(transport);세 개의 MCP 서버를 통합하는 CLI 클라이언트 애플리케이션을 구축합니다.
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
interface ServerConfig {
name: string;
command: string;
args: string[];
env?: Record<string, string>;
}
export class McpManager {
private clients: Map<string, Client> = new Map();
private toolToClient: Map<string, Client> = new Map();
async addServer(config: ServerConfig) {
const transport = new StdioClientTransport({
command: config.command,
args: config.args,
env: { ...process.env, ...config.env },
});
const client = new Client({
name: "devops-assistant-" + config.name,
version: "1.0.0",
});
await client.connect(transport);
this.clients.set(config.name, client);
// 도구 이름 -> 클라이언트 매핑
const tools = await client.listTools();
for (const tool of tools.tools) {
this.toolToClient.set(tool.name, client);
}
console.error("[" + config.name + "] 연결 완료 (" + tools.tools.length + "개 도구)");
}
async getAllTools() {
const allTools: Array<{ name: string; description: string; inputSchema: unknown }> = [];
for (const [, client] of this.clients) {
const result = await client.listTools();
allTools.push(...result.tools.map((t) => ({
name: t.name,
description: t.description || "",
inputSchema: t.inputSchema,
})));
}
return allTools;
}
async callTool(name: string, args: Record<string, unknown>) {
const client = this.toolToClient.get(name);
if (!client) {
throw new Error("도구를 찾을 수 없습니다: " + name);
}
return client.callTool({ name, arguments: args });
}
async getAllResources() {
const allResources: Array<{ uri: string; description: string }> = [];
for (const [, client] of this.clients) {
try {
const result = await client.listResources();
allResources.push(...result.resources.map((r) => ({
uri: r.uri,
description: r.description || "",
})));
} catch {
// 리소스를 지원하지 않는 서버는 건너뜀
}
}
return allResources;
}
async disconnectAll() {
for (const [name, client] of this.clients) {
await client.close();
console.error("[" + name + "] 연결 해제");
}
}
}import Anthropic from "@anthropic-ai/sdk";
import { McpManager } from "./mcp-manager.js";
export class DevOpsAgent {
private anthropic: Anthropic;
private mcpManager: McpManager;
private conversationHistory: Anthropic.MessageParam[] = [];
constructor(mcpManager: McpManager) {
this.anthropic = new Anthropic();
this.mcpManager = mcpManager;
}
async chat(userMessage: string): Promise<string> {
this.conversationHistory.push({
role: "user",
content: userMessage,
});
const tools = await this.mcpManager.getAllTools();
const anthropicTools: Anthropic.Tool[] = tools.map((t) => ({
name: t.name,
description: t.description,
input_schema: t.inputSchema as Anthropic.Tool.InputSchema,
}));
const systemPrompt = [
"당신은 DevOps 어시스턴트입니다.",
"개발팀의 Git 저장소, 로그 파일, 시스템 리소스를 관리하고 분석하는 것을 돕습니다.",
"",
"규칙:",
"- 데이터를 조회한 후 핵심 내용을 요약하여 보고합니다.",
"- 에러가 발견되면 가능한 원인과 해결 방안을 함께 제시합니다.",
"- 시스템 리소스가 임계치(CPU 80%, 메모리 90%, 디스크 90%)를 초과하면 경고합니다.",
"- 한국어로 응답합니다.",
].join("\n");
while (true) {
const response = await this.anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
system: systemPrompt,
tools: anthropicTools,
messages: this.conversationHistory,
});
if (response.stop_reason === "end_turn") {
const textBlock = response.content.find((b) => b.type === "text");
const text = textBlock && "text" in textBlock ? textBlock.text : "";
this.conversationHistory.push({
role: "assistant",
content: response.content,
});
return text;
}
if (response.stop_reason === "tool_use") {
this.conversationHistory.push({
role: "assistant",
content: response.content,
});
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type === "tool_use") {
try {
const result = await this.mcpManager.callTool(
block.name,
block.input as Record<string, unknown>
);
const text = (result.content as Array<{ text?: string }>)
.map((c) => c.text || "")
.join("\n");
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: text,
is_error: result.isError === true,
});
} catch (error) {
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: "도구 실행 오류: " + (error as Error).message,
is_error: true,
});
}
}
}
this.conversationHistory.push({
role: "user",
content: toolResults,
});
}
}
}
clearHistory() {
this.conversationHistory = [];
}
}import readline from "readline";
import { McpManager } from "./mcp-manager.js";
import { DevOpsAgent } from "./agent.js";
async function main() {
const mcpManager = new McpManager();
// MCP 서버들 연결
await mcpManager.addServer({
name: "git",
command: "node",
args: ["../servers/git-server/dist/index.js"],
env: { REPO_PATH: process.env.REPO_PATH || process.cwd() },
});
await mcpManager.addServer({
name: "logs",
command: "node",
args: ["../servers/log-server/dist/index.js"],
env: { LOG_DIR: process.env.LOG_DIR || "/var/log/app" },
});
await mcpManager.addServer({
name: "system",
command: "node",
args: ["../servers/system-server/dist/index.js"],
});
const agent = new DevOpsAgent(mcpManager);
// 대화형 CLI
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
console.log("DevOps 어시스턴트가 준비되었습니다. 질문을 입력하세요. (종료: exit)");
console.log("---");
const prompt = () => {
rl.question("> ", async (input) => {
const trimmed = input.trim();
if (trimmed === "exit" || trimmed === "quit") {
await mcpManager.disconnectAll();
rl.close();
return;
}
if (trimmed === "clear") {
agent.clearHistory();
console.log("대화 기록이 초기화되었습니다.");
prompt();
return;
}
if (!trimmed) {
prompt();
return;
}
try {
const response = await agent.chat(trimmed);
console.log("\n" + response + "\n");
} catch (error) {
console.error("오류:", (error as Error).message);
}
prompt();
});
};
prompt();
}
main().catch(console.error);# 각 서버 빌드
cd servers/git-server && npm run build && cd ../..
cd servers/log-server && npm run build && cd ../..
cd servers/system-server && npm run build && cd ../..
# 클라이언트 빌드 및 실행
cd client
REPO_PATH=/path/to/your/repo LOG_DIR=/var/log/app npm run start실행 후 자연어로 질문할 수 있습니다.
> 오늘 커밋된 내용을 요약해 주세요
> 최근 에러 로그를 분석하고 원인을 추정해 주세요
> 현재 시스템 리소스 사용량은 어떤가요? 이상 징후가 있나요?
> 지난 일주일간 가장 많이 수정된 파일은 무엇인가요?
> ERROR 로그에서 "timeout" 관련 항목을 검색해 주세요
실전 배포를 위해 추가로 고려해야 할 사항을 정리합니다.
MCP 서버가 비정상 종료될 경우 자동으로 재연결하는 로직이 필요합니다.
async addServerWithRetry(config: ServerConfig, maxRetries: number = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await this.addServer(config);
return;
} catch (error) {
console.error(
"[" + config.name + "] 연결 실패 (시도 " + attempt + "/" + maxRetries + "): " +
(error as Error).message
);
if (attempt < maxRetries) {
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
}
}
}
throw new Error("[" + config.name + "] 서버 연결에 실패했습니다.");
}각 도구 호출의 성능과 에러율을 측정하여 운영 상태를 파악합니다.
interface ToolMetrics {
callCount: number;
errorCount: number;
totalDuration: number;
lastCallAt: string;
}
const metrics: Map<string, ToolMetrics> = new Map();
async callToolWithMetrics(name: string, args: Record<string, unknown>) {
const start = Date.now();
const existing = metrics.get(name) || {
callCount: 0, errorCount: 0, totalDuration: 0, lastCallAt: "",
};
try {
const result = await this.callTool(name, args);
existing.callCount++;
existing.totalDuration += Date.now() - start;
existing.lastCallAt = new Date().toISOString();
if (result.isError) existing.errorCount++;
metrics.set(name, existing);
return result;
} catch (error) {
existing.callCount++;
existing.errorCount++;
existing.totalDuration += Date.now() - start;
metrics.set(name, existing);
throw error;
}
}서버 구성을 코드에서 분리하여 설정 파일로 관리합니다.
{
"servers": [
{
"name": "git",
"command": "node",
"args": ["servers/git-server/dist/index.js"],
"env": { "REPO_PATH": "/path/to/repo" }
},
{
"name": "logs",
"command": "node",
"args": ["servers/log-server/dist/index.js"],
"env": { "LOG_DIR": "/var/log/app" }
},
{
"name": "system",
"command": "node",
"args": ["servers/system-server/dist/index.js"]
}
]
}이 시리즈를 통해 MCP의 핵심 개념부터 프로덕션 수준의 시스템 구축까지 전체 과정을 다루었습니다.
1~4장에서는 MCP의 기초를 다졌습니다. 프로토콜의 아키텍처, JSON-RPC 기반 통신, 전송 계층, 서버 프리미티브를 이해했습니다.
5~7장에서는 실제 구현을 수행했습니다. TypeScript SDK와 Python FastMCP로 서버를 구축하고, 클라이언트를 구현하여 LLM과 통합했습니다.
8~9장에서는 프로덕션 관점을 다루었습니다. 기존 시스템과의 연동 패턴과 보안 모범 사례를 학습했습니다.
10장에서는 모든 내용을 종합하여 DevOps 어시스턴트라는 실전 프로젝트를 완성했습니다.
MCP는 AI와 기존 시스템을 연결하는 표준으로 빠르게 자리 잡고 있습니다. 이 시리즈에서 다룬 지식을 바탕으로, 여러분의 시스템에 MCP를 도입하여 AI의 가능성을 확장해 보시기 바랍니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
MCP 서버의 OAuth 2.1 인증, 입력 유효성 검사, 명령 주입 방지, 데이터 유출 방지 등 프로덕션 보안 모범 사례를 다룹니다.
데이터베이스, REST API, 레거시 시스템을 MCP 서버로 래핑하여 AI 모델이 접근할 수 있도록 만드는 실전 패턴을 다룹니다.
MCP 클라이언트를 직접 구현하여 서버에 연결하고, LLM과 통합하여 도구 호출 파이프라인을 완성하는 방법을 다룹니다.