#!/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) => Record]>; 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; const direction = a.direction as string; const period = a.period as string; const endpointMap: Record = { "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 = { 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; const direction = a.direction as string; const period = a.period as string; const endpointMap: Record = { "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 = { 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); 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; 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); });