- 전자세금계산서 API 전체 커버 (29개 툴) - 인증서 관리, 계산서 발행(단건/묶음/대량), 상태조회, 회원관리 - 홈택스 연동 (세금계산서/현금영수증 일별/월별 매출/매입) - 휴폐업 조회, 추가메일 전송 - 에러코드표 내장 (Excel 기반) - 코드표 참조 툴 (billtype, taxrate, billstat 등) - 인증: SENDBILL_SBKEY 환경변수 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
773 lines
34 KiB
JavaScript
773 lines
34 KiB
JavaScript
#!/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);
|
|
});
|