🤖 센드빌(Sendbill) MCP 서버 초기 구현
- 전자세금계산서 API 전체 커버 (29개 툴) - 인증서 관리, 계산서 발행(단건/묶음/대량), 상태조회, 회원관리 - 홈택스 연동 (세금계산서/현금영수증 일별/월별 매출/매입) - 휴폐업 조회, 추가메일 전송 - 에러코드표 내장 (Excel 기반) - 코드표 참조 툴 (billtype, taxrate, billstat 등) - 인증: SENDBILL_SBKEY 환경변수 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
65c69f5666
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.js.map
|
||||
*.d.ts.map
|
||||
.env
|
||||
.env.local
|
||||
1082
package-lock.json
generated
Normal file
1082
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
package.json
Normal file
21
package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "sendbill-mcp",
|
||||
"version": "1.0.0",
|
||||
"description": "센드빌 전자세금계산서 API MCP 서버",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"sendbill-mcp": "./dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0",
|
||||
"@types/node": "^20.0.0"
|
||||
}
|
||||
}
|
||||
46
src/client.ts
Normal file
46
src/client.ts
Normal file
@ -0,0 +1,46 @@
|
||||
// 센드빌 API HTTP 클라이언트
|
||||
|
||||
const BASE_URL = "https://api.sendbill.co.kr";
|
||||
|
||||
export class SendbillClient {
|
||||
private sbkey: string;
|
||||
|
||||
constructor(sbkey: string) {
|
||||
this.sbkey = sbkey;
|
||||
}
|
||||
|
||||
async post(endpoint: string, body: Record<string, unknown>): Promise<unknown> {
|
||||
const url = `${BASE_URL}${endpoint}`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"SBKEY": this.sbkey,
|
||||
"Content-Type": "application/json; charset=UTF-8",
|
||||
"Accept-Charset": "UTF-8",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP 오류: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return { raw: text };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createClient(): SendbillClient {
|
||||
const sbkey = process.env.SENDBILL_SBKEY;
|
||||
if (!sbkey) {
|
||||
throw new Error(
|
||||
"SENDBILL_SBKEY 환경변수가 설정되지 않았습니다.\n" +
|
||||
"사용법: SENDBILL_SBKEY=your-key npx sendbill-mcp"
|
||||
);
|
||||
}
|
||||
return new SendbillClient(sbkey);
|
||||
}
|
||||
148
src/error-codes.ts
Normal file
148
src/error-codes.ts
Normal file
@ -0,0 +1,148 @@
|
||||
// 센드빌 API 에러코드 참조표
|
||||
// 출처: https://www.sendbill.co.kr/SendbillAPI_Error_Code.xls
|
||||
|
||||
export const ERROR_CODES: Record<string, string> = {
|
||||
// SBKEY 관련
|
||||
"-10001": "SBKEY Header 데이터가 없습니다.",
|
||||
"-10002": "SBKEY의 데이터가 옳바르지 않습니다.",
|
||||
"-10003": "폐기 처리된 SBKEY 입니다.",
|
||||
"-10004": "SBKEY의 사용 기간이 만료되었습니다.",
|
||||
|
||||
// 인증서 관련
|
||||
"-11001": "실패",
|
||||
"-11002": "등록된 인증서가 없습니다.",
|
||||
"-11003": "기타오류, 고객센터로 문의 바랍니다.",
|
||||
"-11004": "파일 업로드 중 오류가 발생하였습니다.",
|
||||
"-11101": "올바른 파일을 선택해 주세요.",
|
||||
"-11102": "[venderno] 필수 항목입니다.",
|
||||
"-11103": "[venderno] 최대 길이는 10 bytes 입니다.",
|
||||
"-11104": "[venderno] 숫자만 입력 가능합니다.",
|
||||
"-11105": "[venderno] 올바른 형태의 사업자번호가 아닙니다.",
|
||||
"-11106": "[password] 필수 항목입니다.",
|
||||
"-11107": "[certderfile] 필수 항목입니다.",
|
||||
"-11108": "[certkeyfile] 필수 항목입니다.",
|
||||
"-11401": "등록된 인증서의 발급 정보와 사업자 번호가 일치하지 않습니다.",
|
||||
"-11402": "인증서의 발급 정보와 인증서 비밀번호가 일치하지 않습니다.",
|
||||
"-11403": "인증서의 사용기간이 만료되었습니다.",
|
||||
"-11509": "등록된 인증서의 데이터가 옳바르지 않습니다. 인증서 신규 등록 후 다시 시도해주세요.",
|
||||
"-11701": "URL 유효 시간이 만료되었습니다.",
|
||||
"-11702": "token 오류, token 재발행 후 재시도해주세요.",
|
||||
"-11703": "올바르지 않은 토큰입니다.",
|
||||
|
||||
// 계산서 발행 관련
|
||||
"-12001": "실패",
|
||||
"-12002": "조회된 데이터가 없습니다.",
|
||||
"-12003": "기타오류, 고객센터로 문의 바랍니다.",
|
||||
"-66666": "/api/agent/document/registDirect 긴급점검중 입니다.",
|
||||
"-12501": "충전된 요금이 부족합니다.",
|
||||
"-12502": "우대권 잔여 건 수 및 충전된 요금이 부족합니다.",
|
||||
"-12503": "[대납] 충전된 요금이 부족합니다.",
|
||||
"-12504": "[대납] 우대권 잔여 건 수 및 충전된 요금이 부족합니다.",
|
||||
"-12631": "[A] 공급자 사업자번호에 해당하는 회원 ID가 없습니다.",
|
||||
"-12633": "[A] 공급받는자 사업자번호에 해당하는 회원 ID가 없습니다.",
|
||||
"-12641": "[A] 관리자가 마감하여 전송할 수 없습니다.",
|
||||
"-12651": "[A] 품목의 합계 금액이 총 금액과 맞지 않습니다.",
|
||||
"-12654": "[A] 과세기간(반기)이 지난 세금계산서는 발행이 불가능 합니다.",
|
||||
"-12701": "발행자의 인증서가 등록되어 있지 않습니다.",
|
||||
"-12703": "발행자의 등록된 인증서 사용기간이 만료되었습니다.",
|
||||
"-12101": "[Billseq] 데이터가 중복되었습니다.",
|
||||
"-12102": "[Item(Array)] 필수 항목입니다.",
|
||||
|
||||
// 회원 등록 관련
|
||||
"-13001": "실패",
|
||||
"-13002": "조회된 데이터가 없습니다.",
|
||||
"-13003": "기타오류, 고객센터로 문의 바랍니다.",
|
||||
"-13101": "이미 등록된 아이디 입니다.",
|
||||
|
||||
// 계산서 관리 관련
|
||||
"-14001": "실패",
|
||||
"-14002": "조회된 데이터가 없습니다.",
|
||||
"-14003": "기타오류, 고객센터로 문의 바랍니다.",
|
||||
"-14101": "[Orderseq] 데이터 중복 입니다.",
|
||||
"-14102": "[Billseq] 필수 항목 입니다.",
|
||||
"-14104": "[cmd_div] 필수 항목 입니다.",
|
||||
"-14110": "[cmd_div] 올바른 코드 값을 입력 해주세요. [ 6, E ]",
|
||||
|
||||
// 계산서 상태조회 관련
|
||||
"-15001": "실패",
|
||||
"-15002": "조회된 데이터가 없습니다.",
|
||||
"-15101": "[Billseq] 필수 항목 입니다.",
|
||||
|
||||
// 홈택스 연동 관련
|
||||
"-18001": "실패",
|
||||
"-18002": "조회된 데이터가 없습니다.",
|
||||
"-18004": "홈택스에서 조회된 데이터가 없습니다.",
|
||||
"-18101": "요청 사업자번호의 인증서가 등록되어 있지 않습니다.",
|
||||
"-18403": "센드빌에 등록된 홈택스 계정 또는 인증서 정보가 없습니다.",
|
||||
"-18401": "홈택스연동 사용 사업자가 아닙니다. 고객센터에 문의 바랍니다.",
|
||||
};
|
||||
|
||||
export function getErrorMessage(code: number | string): string {
|
||||
const key = String(Math.round(Number(code)));
|
||||
return ERROR_CODES[key] ?? `알 수 없는 오류 코드: ${code}`;
|
||||
}
|
||||
|
||||
// 코드표
|
||||
export const CODE_TABLES = {
|
||||
billtype: {
|
||||
"10": "세금계산서",
|
||||
"11": "비회원 세금계산서",
|
||||
"20": "(면세)계산서",
|
||||
"21": "비회원(면세)계산서",
|
||||
"30": "거래명세서",
|
||||
"31": "비회원 거래명세서",
|
||||
"40": "위수탁 세금계산서",
|
||||
"41": "비회원 위수탁 세금계산서",
|
||||
"50": "위수탁 (면세)계산서",
|
||||
"51": "비회원 위수탁 (면세)계산서",
|
||||
"61": "개인 매출 세금계산서",
|
||||
"62": "개인 매출 계산서",
|
||||
"Y1": "외부 매출 세금계산서",
|
||||
"Z1": "외부 매입 세금계산서",
|
||||
},
|
||||
cmd_div: {
|
||||
"6": "삭제요청",
|
||||
"E": "EMAIL 전송",
|
||||
},
|
||||
taxrate: {
|
||||
"0": "과세율",
|
||||
"1": "영세율",
|
||||
"2": "면세율",
|
||||
"3": "매입세액불공제",
|
||||
"4": "의제매입",
|
||||
},
|
||||
report_stat: {
|
||||
"N": "미신고",
|
||||
"A": "접수대기",
|
||||
"B": "접수완료",
|
||||
"R": "신고상태",
|
||||
"F": "신고실패",
|
||||
},
|
||||
report_amend_cd: {
|
||||
"1": "기재사항 착오/정정",
|
||||
"2": "공급가액 변동",
|
||||
"3": "환입",
|
||||
"4": "계약의 해제",
|
||||
"5": "내국신용장 사후 개설",
|
||||
"6": "착오에 의한 이중발행",
|
||||
},
|
||||
billstat: {
|
||||
"5": "미전송",
|
||||
"0": "미개봉",
|
||||
"3": "개봉",
|
||||
"1": "승인",
|
||||
"2": "반려",
|
||||
"4": "승인취소",
|
||||
"9": "종이문서저장",
|
||||
"6": "삭제",
|
||||
},
|
||||
gubun: {
|
||||
"1": "영수",
|
||||
"2": "청구",
|
||||
},
|
||||
etc01: {
|
||||
"S": "SENDID: 센드빌 ID / RECVID: 센드빌 ID",
|
||||
"I": "SENDID: 센드빌 ID / RECVID: 거래처 ID",
|
||||
"X": "SENDID: 거래처 ID / RECVID: 거래처 ID",
|
||||
},
|
||||
};
|
||||
772
src/index.ts
Normal file
772
src/index.ts
Normal file
@ -0,0 +1,772 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 센드빌(Sendbill) MCP 서버
|
||||
* 전자세금계산서 API 연동을 위한 Model Context Protocol 서버
|
||||
*
|
||||
* Base URL: https://api.sendbill.co.kr
|
||||
* 인증: SBKEY 헤더 (환경변수 SENDBILL_SBKEY)
|
||||
*
|
||||
* 지원 기능:
|
||||
* - 인증서 관리 (토큰, 등록, 조회, 삭제)
|
||||
* - 계산서(세금) 발행 (단건/묶음/대량)
|
||||
* - 계산서 상태조회 및 관리
|
||||
* - 회원 등록 관리
|
||||
* - 홈택스 연동 (세금계산서/현금영수증 조회)
|
||||
* - 휴폐업 조회
|
||||
* - 추가메일 전송
|
||||
*/
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { createClient } from "./client.js";
|
||||
import { getErrorMessage, CODE_TABLES } from "./error-codes.js";
|
||||
|
||||
const server = new Server(
|
||||
{ name: "sendbill-mcp", version: "1.0.0" },
|
||||
{ capabilities: { tools: {} } }
|
||||
);
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 툴 목록 정의
|
||||
// ─────────────────────────────────────────────
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [
|
||||
// ── 인증서 관리 ──────────────────────────
|
||||
{
|
||||
name: "sendbill_get_token",
|
||||
description:
|
||||
"인증서 등록에 사용할 휘발성 토큰을 생성합니다. (유효시간: 5분)\n" +
|
||||
"POST /api/agent/certificate/gettoken",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
venderno: {
|
||||
type: "string",
|
||||
description: "센드빌 회원사 사업자번호 (하이픈 제외 10자리)",
|
||||
},
|
||||
},
|
||||
required: ["venderno"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sendbill_upload_cert",
|
||||
description:
|
||||
"계산서 발행을 위해 공인인증서를 센드빌에 등록합니다.\n" +
|
||||
"POST /api/agent/certificate/uploadcert\n" +
|
||||
"certderfile, certkeyfile은 각각 signCert.der, signPri.key 파일을 Base64 인코딩한 값입니다.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
jwt: { type: "string", description: "sendbill_get_token으로 발급받은 토큰" },
|
||||
password: { type: "string", description: "인증서 비밀번호" },
|
||||
certderfile: { type: "string", description: "signCert.der 파일 Base64 인코딩값" },
|
||||
certkeyfile: { type: "string", description: "signPri.key 파일 Base64 인코딩값" },
|
||||
},
|
||||
required: ["jwt", "password", "certderfile", "certkeyfile"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sendbill_inquire_cert",
|
||||
description:
|
||||
"센드빌에 등록된 인증서의 만료일자를 조회합니다.\n" +
|
||||
"POST /api/agent/certificate/inquirecert",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
jwt: { type: "string", description: "sendbill_get_token으로 발급받은 토큰" },
|
||||
},
|
||||
required: ["jwt"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sendbill_delete_cert",
|
||||
description:
|
||||
"센드빌에 등록된 인증서를 삭제합니다.\n" +
|
||||
"POST /api/agent/certificate/deletecert",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
jwt: { type: "string", description: "sendbill_get_token으로 발급받은 토큰" },
|
||||
password: { type: "string", description: "등록된 인증서 비밀번호" },
|
||||
},
|
||||
required: ["jwt", "password"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sendbill_get_cert_upload_view",
|
||||
description:
|
||||
"타사가 직접 인증서를 등록할 수 있는 폼 URL을 생성합니다. (유효시간: 5분)\n" +
|
||||
"POST /api/agent/certificate/getuploadview",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
venderno: { type: "string", description: "사업자번호 (하이픈 제외 10자리)" },
|
||||
},
|
||||
required: ["venderno"],
|
||||
},
|
||||
},
|
||||
|
||||
// ── 계산서 발행 ──────────────────────────
|
||||
{
|
||||
name: "sendbill_register_invoice",
|
||||
description:
|
||||
"전자세금계산서(또는 계산서)를 단건 발행합니다.\n" +
|
||||
"POST /api/agent/document/registDirect\n" +
|
||||
"billtype 코드: 10=세금계산서, 20=(면세)계산서, 30=거래명세서, 40=위수탁세금계산서 등\n" +
|
||||
"gubun: 1=영수, 2=청구\n" +
|
||||
"taxrate: 0=과세, 1=영세, 2=면세",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
billseq: { type: "string", description: "세금계산서 고유번호 (최대 25자, 중복불가)" },
|
||||
svenderno: { type: "string", description: "공급자 사업자번호 (하이픈 제외 10자리)" },
|
||||
rvenderno: { type: "string", description: "공급받는자 사업자번호 또는 주민등록번호 (13자리 이내)" },
|
||||
dt: { type: "string", description: "작성일 (YYYY-MM-DD, 미래일자 불가)" },
|
||||
supmoney: { type: "string", description: "공급가액 총액 (숫자, 최대 13자리)" },
|
||||
taxmoney: { type: "string", description: "세액 총액 (숫자, 최대 13자리)" },
|
||||
billtype: { type: "string", description: "계산서 종류 코드 (10/11/20/21/30/31/40/41/50/51/61/62/Y1/Z1)" },
|
||||
gubun: { type: "string", description: "청구구분 (1=영수, 2=청구)" },
|
||||
taxrate: { type: "string", description: "과세구분 (0=과세, 1=영세, 2=면세, 3=매입세액불공제, 4=의제매입)" },
|
||||
scompany: { type: "string", description: "공급자 업체명" },
|
||||
sceoname: { type: "string", description: "공급자 대표자명" },
|
||||
suptae: { type: "string", description: "공급자 업태" },
|
||||
supjong: { type: "string", description: "공급자 업종" },
|
||||
saddress: { type: "string", description: "공급자 주소" },
|
||||
suser: { type: "string", description: "공급자 담당자명" },
|
||||
stelno: { type: "string", description: "공급자 전화번호 (000-0000-0000)" },
|
||||
semail: { type: "string", description: "공급자 이메일" },
|
||||
rcompany: { type: "string", description: "공급받는자 업체명" },
|
||||
rceoname: { type: "string", description: "공급받는자 대표자명" },
|
||||
ruptae: { type: "string", description: "공급받는자 업태" },
|
||||
rupjong: { type: "string", description: "공급받는자 업종" },
|
||||
raddress: { type: "string", description: "공급받는자 주소" },
|
||||
ruser: { type: "string", description: "공급받는자 담당자명" },
|
||||
rtelno: { type: "string", description: "공급받는자 전화번호" },
|
||||
remail: { type: "string", description: "공급받는자 이메일" },
|
||||
report_except_yn: { type: "string", description: "신고 제외 여부 (Y/N, 기본값 N)" },
|
||||
reverseyn: { type: "string", description: "역발행 여부 (Y/N, 기본값 N)" },
|
||||
transyn: { type: "string", description: "전송 여부 (Y/N, 기본값 N)" },
|
||||
test_yn: { type: "string", description: "테스트 여부 (Y/N, 기본값 N)" },
|
||||
bigo: { type: "string", description: "비고 (최대 150자)" },
|
||||
cash: { type: "string", description: "현금 지급액" },
|
||||
checks: { type: "string", description: "수표 지급액" },
|
||||
note: { type: "string", description: "어음 지급액" },
|
||||
credit: { type: "string", description: "외상미수금" },
|
||||
sendid: { type: "string", description: "공급자 센드빌 ID (etc01 코드에 따라 결정)" },
|
||||
recvid: { type: "string", description: "공급받는자 센드빌 ID (etc01 코드에 따라 결정)" },
|
||||
etc01: { type: "string", description: "ID 구분 (X=거래처ID, I=공급자ID+거래처ID, S=양쪽 센드빌ID, 기본값 X)" },
|
||||
etc03: { type: "string", description: "비회원 자동가입 여부 (Y/N, 기본값 N)" },
|
||||
file_url: { type: "string", description: "첨부파일 URL (최대 250자)" },
|
||||
upbillseq: { type: "string", description: "수정세금계산서의 원본 고유번호" },
|
||||
report_amend_cd: { type: "string", description: "수정사유 코드 (1=착오정정, 2=공급가액변동, 3=환입, 4=계약해제, 5=내국신용장, 6=이중발행)" },
|
||||
item: {
|
||||
type: "array",
|
||||
description: "품목 목록 (필수)",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
tax: { type: "string", description: "품목별 세액" },
|
||||
sup: { type: "string", description: "품목별 공급가액" },
|
||||
danga: { type: "string", description: "단가" },
|
||||
vlm: { type: "string", description: "수량" },
|
||||
unit: { type: "string", description: "규격" },
|
||||
obj: { type: "string", description: "품목명" },
|
||||
dt: { type: "string", description: "품목별 거래일자 (YYYY-MM-DD)" },
|
||||
remark: { type: "string", description: "비고" },
|
||||
},
|
||||
required: ["tax", "sup", "dt"],
|
||||
},
|
||||
},
|
||||
broker: {
|
||||
type: "object",
|
||||
description: "수탁자 정보 (billtype 40/41/50/51인 경우만 사용)",
|
||||
properties: {
|
||||
venderno: { type: "string" },
|
||||
company: { type: "string" },
|
||||
ceoname: { type: "string" },
|
||||
uptae: { type: "string" },
|
||||
upjong: { type: "string" },
|
||||
address: { type: "string" },
|
||||
email: { type: "string" },
|
||||
nid: { type: "string", description: "연동ID" },
|
||||
},
|
||||
},
|
||||
},
|
||||
required: [
|
||||
"billseq", "svenderno", "rvenderno", "dt", "supmoney", "taxmoney",
|
||||
"billtype", "gubun", "scompany", "sceoname", "rcompany", "rceoname",
|
||||
"report_except_yn", "reverseyn", "item"
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sendbill_register_invoice_multi",
|
||||
description:
|
||||
"수정세금계산서를 최대 2건 동시 발행합니다. (묶음 발행)\n" +
|
||||
"POST /api/agent/document/registMultiDirect\n" +
|
||||
"주로 기재사항 착오/정정, 내국신용장 사후 개설 시 원본+수정본을 한 번에 발행할 때 사용.\n" +
|
||||
"1건이라도 오류 시 전체 취소됩니다.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
documents: {
|
||||
type: "array",
|
||||
description: "발행할 계산서 목록 (최대 2건). 각 항목은 sendbill_register_invoice와 동일한 형식.",
|
||||
items: { type: "object" },
|
||||
maxItems: 2,
|
||||
},
|
||||
},
|
||||
required: ["documents"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sendbill_register_invoice_bulk",
|
||||
description:
|
||||
"전자세금계산서를 최대 100건 대량 발행합니다.\n" +
|
||||
"POST /api/agent/document/registBulkDirect\n" +
|
||||
"일부 실패 시에도 성공건은 발행됩니다. 응답에 성공/실패 건수와 오류 목록이 포함됩니다.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
documents: {
|
||||
type: "array",
|
||||
description: "발행할 계산서 목록 (최대 100건). 각 항목은 sendbill_register_invoice와 동일한 형식.",
|
||||
items: { type: "object" },
|
||||
maxItems: 100,
|
||||
},
|
||||
},
|
||||
required: ["documents"],
|
||||
},
|
||||
},
|
||||
|
||||
// ── 계산서 조회/관리 ─────────────────────
|
||||
{
|
||||
name: "sendbill_get_invoice_status",
|
||||
description:
|
||||
"세금계산서의 상태를 단건 조회합니다.\n" +
|
||||
"POST /api/agent/status/detail\n" +
|
||||
"billstat 코드: 0=미개봉, 1=승인, 2=반려, 3=개봉, 4=승인취소, 5=미전송, 6=삭제, 9=종이문서저장\n" +
|
||||
"transyn: Y=전송성공, N=전송대기, E=에러",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
billseq: { type: "string", description: "세금계산서 고유번호" },
|
||||
},
|
||||
required: ["billseq"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sendbill_get_invoice_status_list",
|
||||
description:
|
||||
"세금계산서 상태를 최대 1000건 다중 조회합니다.\n" +
|
||||
"POST /api/agent/status/list",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
billseqs: {
|
||||
type: "array",
|
||||
description: "조회할 세금계산서 고유번호 목록 (최대 1000건)",
|
||||
items: { type: "string" },
|
||||
maxItems: 1000,
|
||||
},
|
||||
},
|
||||
required: ["billseqs"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sendbill_manage_invoice",
|
||||
description:
|
||||
"세금계산서에 대한 관리 요청을 처리합니다. (삭제 또는 이메일 재전송)\n" +
|
||||
"POST /api/agent/manager/registDirect\n" +
|
||||
"cmd_div: 6=삭제요청, E=EMAIL 전송\n" +
|
||||
"EMAIL 전송 시 remark에 전송할 이메일 주소를 입력하세요.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
billseq: { type: "string", description: "세금계산서 고유번호" },
|
||||
cmd_div: { type: "string", description: "요청코드 (6=삭제, E=이메일전송)" },
|
||||
remark: { type: "string", description: "이메일 전송 시 수신자 이메일 주소 (최대 500자)" },
|
||||
etc1: { type: "string", description: "기타1 (최대 200자)" },
|
||||
etc2: { type: "string", description: "기타2 (최대 200자)" },
|
||||
etc3: { type: "string", description: "기타3 (최대 200자)" },
|
||||
etc4: { type: "string", description: "기타4 (최대 200자)" },
|
||||
etc5: { type: "string", description: "기타5 (최대 200자)" },
|
||||
},
|
||||
required: ["billseq", "cmd_div"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sendbill_get_invoice_error_log",
|
||||
description:
|
||||
"세금계산서 발행 시 발생한 에러 로그를 조회합니다.\n" +
|
||||
"POST /api/agent/log/bill",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
billseq: { type: "string", description: "세금계산서 고유번호" },
|
||||
},
|
||||
required: ["billseq"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sendbill_get_manage_error_log",
|
||||
description:
|
||||
"계산서 관리 요청(삭제/이메일 전송 등) 시 발생한 에러 로그를 조회합니다.\n" +
|
||||
"POST /api/agent/log/manager",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
billseq: { type: "string", description: "세금계산서 고유번호" },
|
||||
},
|
||||
required: ["billseq"],
|
||||
},
|
||||
},
|
||||
|
||||
// ── 회원 관리 ────────────────────────────
|
||||
{
|
||||
name: "sendbill_register_member",
|
||||
description:
|
||||
"센드빌에 거래처(비회원)를 임시 회원으로 등록합니다.\n" +
|
||||
"POST /api/agent/member/registDirect\n" +
|
||||
"비회원 세금계산서를 사용하지 않는 경우, 반드시 거래처가 센드빌에 등록되어 있어야 합니다.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entcode: { type: "string", description: "제휴업체 코드 (4자리)" },
|
||||
id: { type: "string", description: "회원 아이디 (최대 20자)" },
|
||||
company: { type: "string", description: "업체명 (최대 200자)" },
|
||||
venderno: { type: "string", description: "사업자 등록번호 (하이픈 제외 10자리)" },
|
||||
ceoname: { type: "string", description: "대표자명 (최대 100자)" },
|
||||
uptae: { type: "string", description: "업태 (최대 100자)" },
|
||||
upjong: { type: "string", description: "업종 (최대 100자)" },
|
||||
address: { type: "string", description: "주소 (최대 150자)" },
|
||||
mgmt_yn: { type: "string", description: "관리자 구분 (Y/N)" },
|
||||
email_yn: { type: "string", description: "이메일 수신 여부 (Y/N)" },
|
||||
sms_yn: { type: "string", description: "SMS 수신 여부 (Y/N)" },
|
||||
zipcode: { type: "string", description: "우편번호 (6자리)" },
|
||||
address2: { type: "string", description: "상세주소" },
|
||||
email: { type: "string", description: "이메일" },
|
||||
users: { type: "string", description: "담당자명" },
|
||||
fax: { type: "string", description: "팩스번호" },
|
||||
tel: { type: "string", description: "전화번호" },
|
||||
division: { type: "string", description: "부서명" },
|
||||
resno: { type: "string", description: "주민등록번호 (13자리)" },
|
||||
smsno: { type: "string", description: "핸드폰번호" },
|
||||
sign_yn: { type: "string", description: "담당자 동의 여부 (Y/N)" },
|
||||
},
|
||||
required: ["entcode", "id", "company", "venderno", "ceoname", "uptae", "upjong", "address", "mgmt_yn", "email_yn", "sms_yn"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sendbill_get_member_error_log",
|
||||
description:
|
||||
"회원 등록 요청 시 발생한 에러 로그를 조회합니다.\n" +
|
||||
"POST /api/agent/log/member",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entcode: { type: "string", description: "제휴업체 코드 (최대 6자리)" },
|
||||
userid: { type: "string", description: "회원 아이디(임시가입) (최대 30자)" },
|
||||
},
|
||||
required: ["entcode", "userid"],
|
||||
},
|
||||
},
|
||||
|
||||
// ── 홈택스 연동 ──────────────────────────
|
||||
{
|
||||
name: "sendbill_hometax_query_tax_invoice",
|
||||
description:
|
||||
"홈택스에서 세금계산서 매출/매입 내역을 조회합니다.\n" +
|
||||
"direction: sales=매출, purchase=매입\n" +
|
||||
"period: daily=일별(basedate), monthly=월별(basemonth)\n" +
|
||||
"taxtype: '01'=과세+영세, '03'=면세\n" +
|
||||
"datetype: '01'=작성일자, '02'=발행일자\n" +
|
||||
"orderdirection: '01'=내림차순, '02'=오름차순\n" +
|
||||
"⚠️ 홈택스 접근제어로 30건당 5초 딜레이가 있습니다.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
direction: { type: "string", enum: ["sales", "purchase"], description: "조회 방향 (sales=매출, purchase=매입)" },
|
||||
period: { type: "string", enum: ["daily", "monthly"], description: "조회 주기 (daily=일별, monthly=월별)" },
|
||||
venderno: { type: "string", description: "사업자번호 (10자리)" },
|
||||
taxtype: { type: "string", description: "과세구분 ('01'=과세+영세, '03'=면세)" },
|
||||
datetype: { type: "string", description: "기준일자 유형 ('01'=작성일자, '02'=발행일자)" },
|
||||
basedate: { type: "string", description: "기준일자 (YYYYMMDD, period=daily일 때)" },
|
||||
basemonth: { type: "string", description: "기준월 (YYYYMM, period=monthly일 때)" },
|
||||
orderdirection: { type: "string", description: "정렬방향 ('01'=내림차순, '02'=오름차순)" },
|
||||
},
|
||||
required: ["direction", "period", "venderno", "taxtype", "datetype", "orderdirection"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sendbill_hometax_query_cash_receipt",
|
||||
description:
|
||||
"홈택스에서 현금영수증 매출/매입 내역을 조회합니다.\n" +
|
||||
"direction: sales=매출, purchase=매입\n" +
|
||||
"period: daily=일별(basedate), monthly=월별(basemonth)\n" +
|
||||
"orderdirection: '01'=내림차순, '02'=오름차순\n" +
|
||||
"⚠️ 홈택스 접근제어로 30건당 5초 딜레이가 있습니다.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
direction: { type: "string", enum: ["sales", "purchase"], description: "조회 방향 (sales=매출, purchase=매입)" },
|
||||
period: { type: "string", enum: ["daily", "monthly"], description: "조회 주기 (daily=일별, monthly=월별)" },
|
||||
venderno: { type: "string", description: "사업자번호 (10자리)" },
|
||||
basedate: { type: "string", description: "기준일자 (YYYYMMDD, period=daily일 때)" },
|
||||
basemonth: { type: "string", description: "기준월 (YYYYMM, period=monthly일 때)" },
|
||||
orderdirection: { type: "string", description: "정렬방향 ('01'=내림차순, '02'=오름차순)" },
|
||||
},
|
||||
required: ["direction", "period", "venderno", "orderdirection"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sendbill_hometax_register_account",
|
||||
description:
|
||||
"홈택스 연동을 위한 홈택스 계정을 센드빌에 등록합니다.\n" +
|
||||
"POST /api/agent/hometax/account/regist\n" +
|
||||
"세금계산서용과 현금영수증용 계정을 별도로 사용합니다.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
jwt: { type: "string", description: "sendbill_get_token으로 발급받은 토큰" },
|
||||
billhtid: { type: "string", description: "세금계산서용 홈택스 아이디" },
|
||||
billhtpw: { type: "string", description: "세금계산서용 홈택스 패스워드" },
|
||||
cashhtid: { type: "string", description: "현금영수증용 홈택스 아이디" },
|
||||
cashhtpw: { type: "string", description: "현금영수증용 홈택스 패스워드" },
|
||||
},
|
||||
required: ["jwt", "billhtid", "billhtpw", "cashhtid", "cashhtpw"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sendbill_hometax_delete_account",
|
||||
description:
|
||||
"홈택스 연동계정을 센드빌에서 삭제합니다.\n" +
|
||||
"POST /api/agent/hometax/account/delete\n" +
|
||||
"계정 존재 유무에 대한 결과는 반환되지 않습니다.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
jwt: { type: "string", description: "sendbill_get_token으로 발급받은 토큰" },
|
||||
},
|
||||
required: ["jwt"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sendbill_hometax_get_register_view",
|
||||
description:
|
||||
"타사가 직접 홈택스 연동계정을 등록할 수 있는 폼 URL을 생성합니다. (유효시간: 10분)\n" +
|
||||
"POST /api/agent/hometax/account/getRegistView",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
venderno: { type: "string", description: "사업자번호 (하이픈 제외 10자리)" },
|
||||
},
|
||||
required: ["venderno"],
|
||||
},
|
||||
},
|
||||
|
||||
// ── 휴폐업 조회 ──────────────────────────
|
||||
{
|
||||
name: "sendbill_company_close_search",
|
||||
description:
|
||||
"사업자의 휴업/폐업 상태를 최대 100건 조회합니다.\n" +
|
||||
"POST /api/agent/companyclosesearch\n" +
|
||||
"국세청 데이터를 실시간으로 조회합니다.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
venderno: {
|
||||
type: "array",
|
||||
description: "조회할 사업자번호 목록 (하이픈 제외 10자리, 최대 100건)",
|
||||
items: { type: "string" },
|
||||
maxItems: 100,
|
||||
},
|
||||
},
|
||||
required: ["venderno"],
|
||||
},
|
||||
},
|
||||
|
||||
// ── 추가메일 전송 ─────────────────────────
|
||||
{
|
||||
name: "sendbill_send_additional_mail",
|
||||
description:
|
||||
"이미 발행한 계산서의 이메일을 추가 주소로 재전송합니다.\n" +
|
||||
"POST /api/agent/additionalMail/sendAdditionalMail",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
billseq: { type: "string", description: "발행 문서 고유번호 (최대 19자)" },
|
||||
sendType: { type: "string", description: "전송타입 코드" },
|
||||
newMailAddress: { type: "string", description: "신규 전송 메일 주소 (최대 30자)" },
|
||||
},
|
||||
required: ["billseq", "sendType", "newMailAddress"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sendbill_get_mail_send_list",
|
||||
description:
|
||||
"API로 발행한 계산서의 이메일 전송 내역과 수신 여부를 조회합니다.\n" +
|
||||
"POST /api/agent/additionalMail/sendMailList",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
billseq: { type: "string", description: "API 발행 문서 고유번호 (최대 19자)" },
|
||||
},
|
||||
required: ["billseq"],
|
||||
},
|
||||
},
|
||||
|
||||
// ── 참조 도구 ─────────────────────────────
|
||||
{
|
||||
name: "sendbill_get_error_message",
|
||||
description:
|
||||
"센드빌 API 에러코드에 해당하는 한국어 메시지를 조회합니다.\n" +
|
||||
"API 응답에서 음수 Result 코드를 받았을 때 사용하세요.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
code: { type: "number", description: "에러코드 (음수, 예: -10001)" },
|
||||
},
|
||||
required: ["code"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sendbill_get_code_tables",
|
||||
description:
|
||||
"센드빌 API에서 사용하는 코드표 전체를 반환합니다.\n" +
|
||||
"billtype, cmd_div, taxrate, billstat, gubun, etc01, report_stat, report_amend_cd 코드를 확인할 수 있습니다.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
table: {
|
||||
type: "string",
|
||||
enum: ["billtype", "cmd_div", "taxrate", "billstat", "gubun", "etc01", "report_stat", "report_amend_cd", "all"],
|
||||
description: "조회할 코드표 이름 (all=전체 조회)",
|
||||
},
|
||||
},
|
||||
required: ["table"],
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 툴 실행 핸들러
|
||||
// ─────────────────────────────────────────────
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
// 참조 도구는 API 호출 없음
|
||||
if (name === "sendbill_get_error_message") {
|
||||
const code = (args as { code: number }).code;
|
||||
const message = getErrorMessage(code);
|
||||
return {
|
||||
content: [{ type: "text", text: `에러코드 ${code}: ${message}` }],
|
||||
};
|
||||
}
|
||||
|
||||
if (name === "sendbill_get_code_tables") {
|
||||
const table = (args as { table: string }).table;
|
||||
if (table === "all") {
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(CODE_TABLES, null, 2) }],
|
||||
};
|
||||
}
|
||||
const tableData = CODE_TABLES[table as keyof typeof CODE_TABLES];
|
||||
if (!tableData) {
|
||||
return {
|
||||
content: [{ type: "text", text: `코드표 '${table}'를 찾을 수 없습니다.` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify({ [table]: tableData }, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
// API 호출 도구
|
||||
const client = createClient();
|
||||
|
||||
type EndpointMap = Record<string, [string, (a: Record<string, unknown>) => Record<string, unknown>]>;
|
||||
|
||||
const endpoints: EndpointMap = {
|
||||
// 인증서 관리
|
||||
sendbill_get_token: ["/api/agent/certificate/gettoken", (a) => ({ venderno: a.venderno })],
|
||||
sendbill_upload_cert: ["/api/agent/certificate/uploadcert", (a) => ({
|
||||
jwt: a.jwt, password: a.password, certderfile: a.certderfile, certkeyfile: a.certkeyfile
|
||||
})],
|
||||
sendbill_inquire_cert: ["/api/agent/certificate/inquirecert", (a) => ({ jwt: a.jwt })],
|
||||
sendbill_delete_cert: ["/api/agent/certificate/deletecert", (a) => ({ jwt: a.jwt, password: a.password })],
|
||||
sendbill_get_cert_upload_view: ["/api/agent/certificate/getuploadview", (a) => ({ venderno: a.venderno })],
|
||||
|
||||
// 계산서 발행
|
||||
sendbill_register_invoice: ["/api/agent/document/registDirect", (a) => a],
|
||||
sendbill_register_invoice_multi: ["/api/agent/document/registMultiDirect", (a) => a],
|
||||
sendbill_register_invoice_bulk: ["/api/agent/document/registBulkDirect", (a) => a],
|
||||
|
||||
// 계산서 조회/관리
|
||||
sendbill_get_invoice_status: ["/api/agent/status/detail", (a) => ({ billseq: a.billseq })],
|
||||
sendbill_get_invoice_status_list: ["/api/agent/status/list", (a) => ({ billseqs: a.billseqs })],
|
||||
sendbill_manage_invoice: ["/api/agent/manager/registDirect", (a) => a],
|
||||
sendbill_get_invoice_error_log: ["/api/agent/log/bill", (a) => ({ billseq: a.billseq })],
|
||||
sendbill_get_manage_error_log: ["/api/agent/log/manager", (a) => ({ billseq: a.billseq })],
|
||||
|
||||
// 회원 관리
|
||||
sendbill_register_member: ["/api/agent/member/registDirect", (a) => a],
|
||||
sendbill_get_member_error_log: ["/api/agent/log/member", (a) => ({ entcode: a.entcode, userid: a.userid })],
|
||||
|
||||
// 홈택스 연동
|
||||
sendbill_hometax_register_account: ["/api/agent/hometax/account/regist", (a) => a],
|
||||
sendbill_hometax_delete_account: ["/api/agent/hometax/account/delete", (a) => ({ jwt: a.jwt })],
|
||||
sendbill_hometax_get_register_view: ["/api/agent/hometax/account/getRegistView", (a) => ({ venderno: a.venderno })],
|
||||
|
||||
// 휴폐업 조회
|
||||
sendbill_company_close_search: ["/api/agent/companyclosesearch", (a) => ({ venderno: a.venderno })],
|
||||
|
||||
// 추가메일
|
||||
sendbill_send_additional_mail: ["/api/agent/additionalMail/sendAdditionalMail", (a) => a],
|
||||
sendbill_get_mail_send_list: ["/api/agent/additionalMail/sendMailList", (a) => ({ billseq: a.billseq })],
|
||||
};
|
||||
|
||||
// 홈택스 조회는 방향+주기에 따라 엔드포인트가 다름
|
||||
if (name === "sendbill_hometax_query_tax_invoice") {
|
||||
const a = args as Record<string, unknown>;
|
||||
const direction = a.direction as string;
|
||||
const period = a.period as string;
|
||||
|
||||
const endpointMap: Record<string, string> = {
|
||||
"sales-daily": "/api/agent/hometax/listDailyTaxInvoiceSales",
|
||||
"sales-monthly": "/api/agent/hometax/listMonthlyTaxInvoiceSales",
|
||||
"purchase-daily": "/api/agent/hometax/listDailyTaxInvoicePurchase",
|
||||
"purchase-monthly": "/api/agent/hometax/listMonthlyTaxInvoicePurchase",
|
||||
};
|
||||
|
||||
const endpoint = endpointMap[`${direction}-${period}`];
|
||||
if (!endpoint) {
|
||||
return { content: [{ type: "text", text: "잘못된 direction 또는 period 값입니다." }], isError: true };
|
||||
}
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
venderno: a.venderno,
|
||||
taxtype: a.taxtype,
|
||||
datetype: a.datetype,
|
||||
orderdirection: a.orderdirection,
|
||||
};
|
||||
if (period === "daily") body.basedate = a.basedate;
|
||||
else body.basemonth = a.basemonth;
|
||||
|
||||
try {
|
||||
const result = await client.post(endpoint, body);
|
||||
const text = formatResult(result);
|
||||
return { content: [{ type: "text", text }] };
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return { content: [{ type: "text", text: `API 오류: ${message}` }], isError: true };
|
||||
}
|
||||
}
|
||||
|
||||
if (name === "sendbill_hometax_query_cash_receipt") {
|
||||
const a = args as Record<string, unknown>;
|
||||
const direction = a.direction as string;
|
||||
const period = a.period as string;
|
||||
|
||||
const endpointMap: Record<string, string> = {
|
||||
"sales-daily": "/api/agent/hometax/listDailyCashBillSales",
|
||||
"sales-monthly": "/api/agent/hometax/listMonthlyCashBillSales",
|
||||
"purchase-daily": "/api/agent/hometax/listDailyCashBillPurchase",
|
||||
"purchase-monthly": "/api/agent/hometax/listMonthlyCashBillPurchase",
|
||||
};
|
||||
|
||||
const endpoint = endpointMap[`${direction}-${period}`];
|
||||
if (!endpoint) {
|
||||
return { content: [{ type: "text", text: "잘못된 direction 또는 period 값입니다." }], isError: true };
|
||||
}
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
venderno: a.venderno,
|
||||
orderdirection: a.orderdirection,
|
||||
};
|
||||
if (period === "daily") body.basedate = a.basedate;
|
||||
else body.basemonth = a.basemonth;
|
||||
|
||||
try {
|
||||
const result = await client.post(endpoint, body);
|
||||
const text = formatResult(result);
|
||||
return { content: [{ type: "text", text }] };
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return { content: [{ type: "text", text: `API 오류: ${message}` }], isError: true };
|
||||
}
|
||||
}
|
||||
|
||||
// 일반 엔드포인트 처리
|
||||
const entry = endpoints[name];
|
||||
if (!entry) {
|
||||
return {
|
||||
content: [{ type: "text", text: `알 수 없는 툴: ${name}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const [endpoint, buildBody] = entry;
|
||||
const body = buildBody(args as Record<string, unknown>);
|
||||
|
||||
try {
|
||||
const result = await client.post(endpoint, body);
|
||||
const text = formatResult(result);
|
||||
return { content: [{ type: "text", text }] };
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return { content: [{ type: "text", text: `API 오류: ${message}` }], isError: true };
|
||||
}
|
||||
});
|
||||
|
||||
function formatResult(result: unknown): string {
|
||||
if (typeof result !== "object" || result === null) {
|
||||
return String(result);
|
||||
}
|
||||
const obj = result as Record<string, unknown>;
|
||||
const lines: string[] = [];
|
||||
|
||||
// Result 코드 해석
|
||||
if ("Result" in obj || "result" in obj) {
|
||||
const code = (obj.Result ?? obj.result) as number;
|
||||
const msg = (obj.Message ?? obj.message) as string | undefined;
|
||||
if (code < 0) {
|
||||
lines.push(`❌ 실패 (Result: ${code})`);
|
||||
lines.push(` 오류: ${msg ?? getErrorMessage(code)}`);
|
||||
} else {
|
||||
lines.push(`✅ 성공 (Result: ${code})`);
|
||||
if (msg) lines.push(` 메시지: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 나머지 데이터
|
||||
const skip = new Set(["Result", "result", "Message", "message"]);
|
||||
const rest = Object.fromEntries(Object.entries(obj).filter(([k]) => !skip.has(k)));
|
||||
if (Object.keys(rest).length > 0) {
|
||||
lines.push("\n데이터:");
|
||||
lines.push(JSON.stringify(rest, null, 2));
|
||||
}
|
||||
|
||||
return lines.join("\n") || JSON.stringify(result, null, 2);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 서버 시작
|
||||
// ─────────────────────────────────────────────
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
// stderr에만 출력 (stdout은 MCP 프로토콜 전용)
|
||||
process.stderr.write("센드빌 MCP 서버 시작됨\n");
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
process.stderr.write(`치명적 오류: ${err}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user