소바
클라이언트 세션을 처리할 수 있는 MCP 서버를 구축하기 위한 TypeScript 프레임워크입니다.
[!메모]
Python 구현에 대해서는 sova 를 참조하세요.
특징
설치
지엑스피1
빠른 시작
[!메모]
실제로 소바를 활용하는 사례는 많습니다. 쇼케이스 에서 예시를 확인하세요.
import { sova } from "sova";
import { z } from "zod"; // Or any validation library that supports Standard Schema
const server = new sova({
name: "My Server",
version: "1.0.0",
});
server.addTool({
name: "add",
description: "Add two numbers",
parameters: z.object({
a: z.number(),
b: z.number(),
}),
execute: async (args) => {
return String(args.a + args.b);
},
});
server.start({
transportType: "stdio",
});
이제 MCP 서버가 정상적으로 작동합니다.
터미널에서 다음을 사용하여 서버를 테스트할 수 있습니다.
git clone https://github.com/bounded1999/sova.git
cd sova
pnpm install
pnpm build
# Test the addition server example using CLI:
npx sova dev src/examples/addition.ts
# Test the addition server example using MCP Inspector:
npx sova inspect src/examples/addition.ts
남남동
서버 전송 이벤트 (SSE)는 서버가 HTTPS 연결을 통해 클라이언트에 실시간 업데이트를 전송할 수 있는 메커니즘을 제공합니다. MCP 환경에서 SSE는 주로 원격 MCP 통신을 활성화하는 데 사용되며, 원격 컴퓨터에 호스팅된 MCP에 액세스하고 네트워크를 통해 업데이트를 전달할 수 있도록 합니다.
SSE 지원으로 서버를 실행할 수도 있습니다.
server.start({
transportType: "sse",
sse: {
endpoint: "/sse",
port: 8080,
},
});
이렇게 하면 서버가 시작되고 http://localhost:8080/sse
에서 SSE 연결을 수신합니다.
그런 다음 SSEClientTransport
사용하여 서버에 연결할 수 있습니다.
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
const client = new Client(
{
name: "example-client",
version: "1.0.0",
},
{
capabilities: {},
},
);
const transport = new SSEClientTransport(new URL(`http://localhost:8080/sse`));
await client.connect(transport);
핵심 개념
도구
MCP의 도구를 사용하면 클라이언트가 호출하고 LLM이 작업을 수행하는 데 사용할 수 있는 실행 가능한 함수를 서버가 노출할 수 있습니다.
sova는 도구 매개변수를 정의하기 위해 표준 스키마 사양을 사용합니다. 이를 통해 해당 사양을 구현하는 한 선호하는 스키마 검증 라이브러리(예: Zod, ArkType, Valibot)를 사용할 수 있습니다.
Zod 예:
import { z } from "zod";
server.addTool({
name: "fetch-zod",
description: "Fetch the content of a url (using Zod)",
parameters: z.object({
url: z.string(),
}),
execute: async (args) => {
return await fetchWebpageContent(args.url);
},
});
ArkType 예:
import { type } from "arktype";
server.addTool({
name: "fetch-arktype",
description: "Fetch the content of a url (using ArkType)",
parameters: type({
url: "string",
}),
execute: async (args) => {
return await fetchWebpageContent(args.url);
},
});
발리봇 예시:
Valibot에는 @valibot/to-json-schema 피어 종속성이 필요합니다.
import * as v from "valibot";
server.addTool({
name: "fetch-valibot",
description: "Fetch the content of a url (using Valibot)",
parameters: v.object({
url: v.string(),
}),
execute: async (args) => {
return await fetchWebpageContent(args.url);
},
});
문자열 반환
execute
문자열을 반환할 수 있습니다:
server.addTool({
name: "download",
description: "Download a file",
parameters: z.object({
url: z.string(),
}),
execute: async (args) => {
return "Hello, world!";
},
});
후자는 다음과 같습니다.
server.addTool({
name: "download",
description: "Download a file",
parameters: z.object({
url: z.string(),
}),
execute: async (args) => {
return {
content: [
{
type: "text",
text: "Hello, world!",
},
],
};
},
});
목록 반환
메시지 목록을 반환하려면 content
속성이 있는 객체를 반환하면 됩니다.
server.addTool({
name: "download",
description: "Download a file",
parameters: z.object({
url: z.string(),
}),
execute: async (args) => {
return {
content: [
{ type: "text", text: "First message" },
{ type: "text", text: "Second message" },
],
};
},
});
이미지 반환
imageContent
사용하여 이미지에 대한 콘텐츠 객체를 만듭니다.
import { imageContent } from "sova";
server.addTool({
name: "download",
description: "Download a file",
parameters: z.object({
url: z.string(),
}),
execute: async (args) => {
return imageContent({
url: "https://example.com/image.png",
});
// or...
// return imageContent({
// path: "/path/to/image.png",
// });
// or...
// return imageContent({
// buffer: Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", "base64"),
// });
// or...
// return {
// content: [
// await imageContent(...)
// ],
// };
},
});
imageContent
함수는 다음과 같은 옵션을 사용합니다.
url
: 이미지의 URL입니다.path
: 이미지 파일의 경로입니다.buffer
: 버퍼로서의 이미지 데이터.
url
, path
또는 buffer
중 하나만 지정해야 합니다.
위의 예는 다음과 같습니다.
server.addTool({
name: "download",
description: "Download a file",
parameters: z.object({
url: z.string(),
}),
execute: async (args) => {
return {
content: [
{
type: "image",
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
mimeType: "image/png",
},
],
};
},
});
오디오 반환
audioContent
사용하여 오디오에 대한 콘텐츠 객체를 만듭니다.
import { audioContent } from "sova";
server.addTool({
name: "download",
description: "Download a file",
parameters: z.object({
url: z.string(),
}),
execute: async (args) => {
return audioContent({
url: "https://example.com/audio.mp3",
});
// or...
// return audioContent({
// path: "/path/to/audio.mp3",
// });
// or...
// return audioContent({
// buffer: Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", "base64"),
// });
// or...
// return {
// content: [
// await audioContent(...)
// ],
// };
},
});
audioContent
함수는 다음과 같은 옵션을 사용합니다.
url
: 오디오의 URL입니다.path
: 오디오 파일의 경로입니다.buffer
: 버퍼로서의 오디오 데이터.
url
, path
또는 buffer
중 하나만 지정해야 합니다.
위의 예는 다음과 같습니다.
server.addTool({
name: "download",
description: "Download a file",
parameters: z.object({
url: z.string(),
}),
execute: async (args) => {
return {
content: [
{
type: "audio",
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
mimeType: "audio/mpeg",
},
],
};
},
});
반품 조합 유형
이런 식으로 다양한 유형을 결합해 AI로 다시 보낼 수 있습니다.
server.addTool({
name: "download",
description: "Download a file",
parameters: z.object({
url: z.string(),
}),
execute: async (args) => {
return {
content: [
{
type: "text",
text: "Hello, world!",
},
{
type: "image",
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
mimeType: "image/png",
},
{
type: "audio",
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
mimeType: "audio/mpeg",
},
],
};
},
// or...
// execute: async (args) => {
// const imgContent = imageContent({
// url: "https://example.com/image.png",
// });
// const audContent = audioContent({
// url: "https://example.com/audio.mp3",
// });
// return {
// content: [
// {
// type: "text",
// text: "Hello, world!",
// },
// imgContent,
// audContent,
// ],
// };
// },
});
벌채 반출
도구는 컨텍스트 개체의 log
개체를 사용하여 클라이언트에 메시지를 기록할 수 있습니다.
server.addTool({
name: "download",
description: "Download a file",
parameters: z.object({
url: z.string(),
}),
execute: async (args, { log }) => {
log.info("Downloading file...", {
url,
});
// ...
log.info("Downloaded file");
return "done";
},
});
log
객체에는 다음과 같은 메서드가 있습니다.
debug(message: string, data?: SerializableValue)
error(message: string, data?: SerializableValue)
info(message: string, data?: SerializableValue)
warn(message: string, data?: SerializableValue)
오류
사용자에게 표시될 오류는 UserError
인스턴스로 발생해야 합니다.
import { UserError } from "sova";
server.addTool({
name: "download",
description: "Download a file",
parameters: z.object({
url: z.string(),
}),
execute: async (args) => {
if (args.url.startsWith("https://example.com")) {
throw new UserError("This URL is not allowed");
}
return "done";
},
});
진전
도구는 컨텍스트 개체에서 reportProgress
호출하여 진행 상황을 보고할 수 있습니다.
server.addTool({
name: "download",
description: "Download a file",
parameters: z.object({
url: z.string(),
}),
execute: async (args, { reportProgress }) => {
reportProgress({
progress: 0,
total: 100,
});
// ...
reportProgress({
progress: 100,
total: 100,
});
return "done";
},
});
도구 주석
MCP 사양(2025-03-26)에 따라 도구에는 도구 동작에 대한 메타데이터를 추가하여 보다 풍부한 컨텍스트와 제어를 제공하는 주석이 포함될 수 있습니다.
server.addTool({
name: "fetch-content",
description: "Fetch content from a URL",
parameters: z.object({
url: z.string(),
}),
annotations: {
title: "Web Content Fetcher", // Human-readable title for UI display
readOnlyHint: true, // Tool doesn't modify its environment
openWorldHint: true, // Tool interacts with external entities
},
execute: async (args) => {
return await fetchWebpageContent(args.url);
},
});
사용 가능한 주석은 다음과 같습니다.
주석 | 유형 | 기본 | 설명 |
---|
title | 끈 | - | UI 표시에 유용한 도구의 사람이 읽을 수 있는 제목 |
readOnlyHint | 부울 | false | true인 경우 도구가 환경을 수정하지 않음을 나타냅니다. |
destructiveHint | 부울 | true | true인 경우 도구는 파괴적인 업데이트를 수행할 수 있습니다( readOnlyHint 가 false인 경우에만 의미가 있음) |
idempotentHint | 부울 | false | true인 경우 동일한 인수로 도구를 반복적으로 호출해도 추가 효과가 없습니다( readOnlyHint 가 false인 경우에만 의미가 있음). |
openWorldHint | 부울 | true | 참이면 도구는 외부 엔터티의 "열린 세계"와 상호 작용할 수 있습니다. |
이러한 주석은 클라이언트와 LLM이 도구의 사용법과 전화 통화 시 예상되는 상황을 더 잘 이해하는 데 도움이 됩니다.
자원
리소스는 MCP 서버가 클라이언트에 제공하려는 모든 종류의 데이터를 나타냅니다. 여기에는 다음이 포함될 수 있습니다.
- 파일 내용
- 스크린샷 및 이미지
- 그리고 더 많은 것
각 리소스는 고유한 URI로 식별되며 텍스트 또는 이진 데이터를 포함할 수 있습니다.
server.addResource({
uri: "file:///logs/app.log",
name: "Application Logs",
mimeType: "text/plain",
async load() {
return {
text: await readLogFile(),
};
},
});
[!메모]
load
여러 리소스를 반환할 수 있습니다. 예를 들어, 디렉터리를 읽을 때 디렉터리 내의 파일 목록을 반환하는 데 사용할 수 있습니다.
async load() {
return [
{
text: "First file content",
},
{
text: "Second file content",
},
];
}
load
에서 바이너리 콘텐츠를 반환할 수도 있습니다.
async load() {
return {
blob: 'base64-encoded-data'
};
}
리소스 템플릿
리소스 템플릿을 정의할 수도 있습니다.
server.addResourceTemplate({
uriTemplate: "file:///logs/{name}.log",
name: "Application Logs",
mimeType: "text/plain",
arguments: [
{
name: "name",
description: "Name of the log",
required: true,
},
],
async load({ name }) {
return {
text: `Example log content for ${name}`,
};
},
});
리소스 템플릿 인수 자동 완성
자동 완성을 활성화하기 위해 리소스 템플릿 인수에 대한 complete
기능을 제공합니다.
server.addResourceTemplate({
uriTemplate: "file:///logs/{name}.log",
name: "Application Logs",
mimeType: "text/plain",
arguments: [
{
name: "name",
description: "Name of the log",
required: true,
complete: async (value) => {
if (value === "Example") {
return {
values: ["Example Log"],
};
}
return {
values: [],
};
},
},
],
async load({ name }) {
return {
text: `Example log content for ${name}`,
};
},
});
프롬프트
프롬프트를 사용하면 서버가 재사용 가능한 프롬프트 템플릿과 워크플로를 정의하여 클라이언트가 사용자와 LLM에 쉽게 제공할 수 있습니다. 또한, 공통 LLM 상호작용을 표준화하고 공유하는 강력한 방법을 제공합니다.
server.addPrompt({
name: "git-commit",
description: "Generate a Git commit message",
arguments: [
{
name: "changes",
description: "Git diff or description of changes",
required: true,
},
],
load: async (args) => {
return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}`;
},
});
프롬프트 인수 자동 완성
프롬프트는 인수에 대한 자동 완성 기능을 제공할 수 있습니다.
server.addPrompt({
name: "countryPoem",
description: "Writes a poem about a country",
load: async ({ name }) => {
return `Hello, ${name}!`;
},
arguments: [
{
name: "name",
description: "Name of the country",
required: true,
complete: async (value) => {
if (value === "Germ") {
return {
values: ["Germany"],
};
}
return {
values: [],
};
},
},
],
});
enum
사용한 프롬프트 인수 자동 완성
인수에 enum
형 배열을 제공하면 서버가 자동으로 인수에 대한 완성을 제공합니다.
server.addPrompt({
name: "countryPoem",
description: "Writes a poem about a country",
load: async ({ name }) => {
return `Hello, ${name}!`;
},
arguments: [
{
name: "name",
description: "Name of the country",
required: true,
enum: ["Germany", "France", "Italy"],
},
],
});
입증
sova를 사용하면 사용자 정의 함수를 사용하여 클라이언트를 authenticate
수 있습니다.
import { AuthError } from "sova";
const server = new sova({
name: "My Server",
version: "1.0.0",
authenticate: ({ request }) => {
const apiKey = request.headers["x-api-key"];
if (apiKey !== "123") {
throw new Response(null, {
status: 401,
statusText: "Unauthorized",
});
}
// Whatever you return here will be accessible in the `context.session` object.
return {
id: 1,
};
},
});
이제 도구에서 인증된 세션 데이터에 액세스할 수 있습니다.
server.addTool({
name: "sayHello",
execute: async (args, { session }) => {
return `Hello, ${session.id}!`;
},
});
지침 제공
instructions
옵션을 사용하여 서버에 지침을 제공할 수 있습니다.
const server = new sova({
name: "My Server",
version: "1.0.0",
instructions:
'Instructions describing how to use the server and its features.\n\nThis can be used by clients to improve the LLM\'s understanding of available tools, resources, etc. It can be thought of like a "hint" to the model. For example, this information MAY be added to the system prompt.',
});
세션
session
객체는 sovaSession
의 인스턴스이며 활성 클라이언트 세션을 설명합니다.
클라이언트와 서버 간의 1:1 통신을 가능하게 하기 위해 각 클라이언트 연결에 대해 새로운 서버 인스턴스를 할당합니다.
입력된 서버 이벤트
on
메서드를 사용하여 서버에서 발생하는 이벤트를 수신할 수 있습니다.
server.on("connect", (event) => {
console.log("Client connected:", event.session);
});
server.on("disconnect", (event) => {
console.log("Client disconnected:", event.session);
});
sovaSession
sovaSession
클라이언트 세션을 나타내며 클라이언트와 상호작용하는 방법을 제공합니다.
sovaSession
인스턴스를 얻는 방법에 대한 예는 세션을 참조하세요.
requestSampling
requestSampling
샘플링 요청을 생성하고 응답을 반환합니다.
await session.requestSampling({
messages: [
{
role: "user",
content: {
type: "text",
text: "What files are in the current directory?",
},
},
],
systemPrompt: "You are a helpful file system assistant.",
includeContext: "thisServer",
maxTokens: 100,
});
clientCapabilities
clientCapabilities
속성에는 클라이언트 기능이 포함되어 있습니다.
session.clientCapabilities;
loggingLevel
loggingLevel
속성은 클라이언트가 설정한 로깅 수준을 설명합니다.
roots
roots
속성에는 클라이언트가 설정한 루트가 포함됩니다.
server
server
속성에는 세션과 연관된 MCP 서버 인스턴스가 포함되어 있습니다.
입력된 세션 이벤트
on
메서드를 사용하여 세션에서 발생하는 이벤트를 수신할 수 있습니다.
session.on("rootsChanged", (event) => {
console.log("Roots changed:", event.roots);
});
session.on("error", (event) => {
console.error("Error:", event.error);
});
서버 실행
mcp-cli
로 테스트
서버를 테스트하고 디버깅하는 가장 빠른 방법은 sova dev
사용하는 것입니다.
npx sova dev server.js
npx sova dev server.ts
이렇게 하면 터미널에서 MCP 서버를 테스트하고 디버깅하기 위해 mcp-cli
로 서버가 실행됩니다.
MCP Inspector
으로 검사하세요
또 다른 방법은 공식 MCP Inspector
사용하여 웹 UI로 서버를 검사하는 것입니다.
npx sova inspect server.ts
자주 묻는 질문
Claude Desktop과 함께 사용하는 방법?
https://modelcontextprotocol.io/quickstart/user 가이드를 따르고 다음 구성을 추가하세요.
{
"mcpServers": {
"my-mcp-server": {
"command": "npx",
"args": ["tsx", "/PATH/TO/YOUR_PROJECT/src/index.ts"],
"env": {
"YOUR_ENV_VAR": "value"
}
}
}
}
감사의 말