@nhnpayco-dev/mcp-devcenter 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
  "schema_version": "1.0",
3
3
  "source_site": "https://devcenter.payco.com",
4
4
  "scope": "PAYCO 가이드 전체 (온라인결제·자동결제·오프라인결제·오더클라우드·멤버십클라우드·결제정산대사)",
5
- "generated_at": "2026-10-14T04:35:38+00:00",
5
+ "generated_at": "2026-10-26T02:06:57+00:00",
6
6
  "category_count": 28,
7
7
  "categories": [
8
8
  {
package/dist/tools.js CHANGED
@@ -72,6 +72,106 @@ function flattenPayload(payload) {
72
72
  visit(k, v);
73
73
  return out;
74
74
  }
75
+ /** 명세 필수 항목(점 경로)이 payload 에 없는 위치를 찾는다.
76
+ *
77
+ * - 배열은 원소마다 확인하고 위치를 표시한다. 예) orderProducts[1].productId
78
+ * - 하위 항목은 상위 항목을 보냈을 때만 확인한다(상위가 없으면 상위 자체의 필수 여부로 판단).
79
+ * 예) sign 을 보내지 않으면 sign.signData 는 확인하지 않는다.
80
+ */
81
+ function findMissingRequired(root, path) {
82
+ const segs = path.split(".");
83
+ const out = [];
84
+ const walk = (val, i, at) => {
85
+ if (Array.isArray(val)) {
86
+ val.forEach((el, k) => walk(el, i, `${at}[${k}]`));
87
+ return;
88
+ }
89
+ if (i === segs.length)
90
+ return;
91
+ if (!val || typeof val !== "object")
92
+ return; // 상위가 객체가 아니면 타입 검사에서 다룬다
93
+ const key = segs[i];
94
+ const here = at ? `${at}.${key}` : key;
95
+ const obj = val;
96
+ if (!(key in obj) || obj[key] === undefined || obj[key] === null) { // null 은 보내지 않은 것으로 본다
97
+ if (i === segs.length - 1)
98
+ out.push(here); // 마지막 단계에서만 누락으로 본다
99
+ return;
100
+ }
101
+ walk(obj[key], i + 1, here);
102
+ };
103
+ walk(root, 0, "");
104
+ return out;
105
+ }
106
+ /** 명세 타입 표기(string(10), integer, double, list<string>, 형식 등)와 실제 값이 맞는지.
107
+ * - 숫자 계열(number/integer/int/long/float/double)은 숫자 또는 숫자 형태의 문자열("67300")을 허용한다.
108
+ * PAYCO 서버가 숫자 문자열도 처리한다(2026-10-23 확인). 가이드 공식 샘플도 문자열로 보낸다.
109
+ * - 판별할 수 없는 표기(unknown, 표 파싱 잔재 등)는 검사하지 않는다(null 반환).
110
+ */
111
+ function typeMatches(declared, value) {
112
+ const t = declared.toLowerCase();
113
+ const actual = Array.isArray(value) ? "array" : typeof value;
114
+ if (/^(number|integer|int|long|float|double)\b/.test(t)) {
115
+ return actual === "number" || (actual === "string" && /^-?\d+(\.\d+)?$/.test(value.trim()));
116
+ }
117
+ if (/^(array|list)/.test(t))
118
+ return actual === "array";
119
+ if (/^string/.test(t))
120
+ return actual === "string";
121
+ if (/^object/.test(t))
122
+ return actual === "object";
123
+ if (/^boolean/.test(t))
124
+ return actual === "boolean";
125
+ return null;
126
+ }
127
+ /** 조건부 항목(when)이 이 payload 에 해당하는지. 예) sign 하위 항목은 vanCorpCode 가 맞을 때만 */
128
+ function whenApplies(root, when) {
129
+ if (!when)
130
+ return true;
131
+ return Object.entries(when).every(([field, values]) => {
132
+ const v = root[field];
133
+ return typeof v === "string" && values.includes(v.trim());
134
+ });
135
+ }
136
+ /** extraData 는 가이드상 JSON 문자열이다. 검사용 사본에서 문자열을 풀어 안쪽 항목까지 검사한다.
137
+ * - 문자열이면 JSON 으로 풀어 객체로 바꾼다(타입은 정상).
138
+ * - 깨진 JSON 이면 문제로 보고하고 안쪽은 검사하지 않는다.
139
+ * - 객체로 보냈으면 안쪽은 검사하되, 문자열로 보내야 한다는 타입 문제를 보고한다.
140
+ */
141
+ function expandExtraData(obj) {
142
+ const ed = obj["extraData"];
143
+ if (ed === undefined)
144
+ return { view: obj, issues: [] };
145
+ if (typeof ed === "string") {
146
+ try {
147
+ const parsed = JSON.parse(ed);
148
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
149
+ return { view: { ...obj, extraData: parsed }, issues: [] };
150
+ }
151
+ return { view: obj, issues: ["extraData: 올바른 JSON 문자열이 아닙니다 — JSON 객체를 문자열로 바꾼 값이어야 합니다"] };
152
+ }
153
+ catch (e) {
154
+ return { view: obj, issues: [`extraData: 올바른 JSON 문자열이 아닙니다 — ${e.message}`] };
155
+ }
156
+ }
157
+ if (ed && typeof ed === "object" && !Array.isArray(ed)) {
158
+ return {
159
+ view: obj,
160
+ issues: ["extraData: 명세 'string' / 실제 'object' — JSON 객체를 문자열로 바꿔서(JSON.stringify) 보내야 합니다"],
161
+ };
162
+ }
163
+ return { view: obj, issues: [] }; // 그 밖의 타입은 일반 타입 검사에서 다룬다
164
+ }
165
+ /** 최상위에 없으면 extraData(검사용으로 풀린 객체) 안에서 찾는다 */
166
+ function pickField(obj, field) {
167
+ if (field in obj)
168
+ return obj[field];
169
+ const ed = obj["extraData"];
170
+ if (ed && typeof ed === "object" && !Array.isArray(ed) && field in ed) {
171
+ return ed[field];
172
+ }
173
+ return undefined;
174
+ }
75
175
  /** 본문에서 query 가 포함된 줄(앞뒤 줄 포함) 발췌 */
76
176
  function snippetLines(md, query, maxHits = 5) {
77
177
  const q = query.toLowerCase();
@@ -89,14 +189,34 @@ function autoCheckPii(obj) {
89
189
  // 객체의 문자열 값들을 개별 검사 (JSON 직렬화 시 인접 숫자들이 false-positive 유발 방지)
90
190
  const hits = [];
91
191
  // 앞뒤에 추가 숫자가 없을 때만 매칭 (timestamp 같은 긴 숫자에서 부분 매칭 회피)
92
- const RE_RRN = /(?<!\d)\d{6}-?[1-4]\d{6}(?!\d)/; // 주민번호 13자리
93
- const RE_CARD = /(?<!\d)\d{4}-?\d{4}-?\d{4}-?\d{4}(?!\d)/; // 카드 16자리
192
+ // 주민번호 13자리. 앞 6자리가 실제 날짜(YYMMDD)일 때만 본다 — 주문번호의 13자리 시각 값(1640064545311) 오탐 방지
193
+ const RE_RRN = /(?<!\d)(\d{2})(\d{2})(\d{2})-?[1-4]\d{6}(?!\d)/g;
194
+ // 카드 16자리. 카드번호 검증 규칙(Luhn)을 통과할 때만 본다 — 16자리 거래번호(2022021610443637) 오탐 방지
195
+ const RE_CARD = /(?<!\d)\d{4}-?\d{4}-?\d{4}-?\d{4}(?!\d)/g;
94
196
  const RE_PHONE = /(?<!\d)01[0-9]-?\d{3,4}-?\d{4}(?!\d)/; // 휴대전화 10~11자리
197
+ const hasRrn = (s) => [...s.matchAll(RE_RRN)].some((m) => {
198
+ const mm = Number(m[2]), dd = Number(m[3]);
199
+ return mm >= 1 && mm <= 12 && dd >= 1 && dd <= 31;
200
+ });
201
+ const luhn = (digits) => {
202
+ let sum = 0;
203
+ for (let i = 0; i < digits.length; i++) {
204
+ let d = Number(digits[digits.length - 1 - i]);
205
+ if (i % 2 === 1) {
206
+ d *= 2;
207
+ if (d > 9)
208
+ d -= 9;
209
+ }
210
+ sum += d;
211
+ }
212
+ return sum % 10 === 0;
213
+ };
214
+ const hasCard = (s) => [...s.matchAll(RE_CARD)].some((m) => luhn(m[0].replace(/-/g, "")));
95
215
  const visit = (v) => {
96
216
  if (typeof v === "string") {
97
- if (RE_RRN.test(v) && !hits.includes("주민번호 패턴"))
217
+ if (hasRrn(v) && !hits.includes("주민번호 패턴"))
98
218
  hits.push("주민번호 패턴");
99
- if (RE_CARD.test(v) && !hits.includes("카드번호 패턴"))
219
+ if (hasCard(v) && !hits.includes("카드번호 패턴"))
100
220
  hits.push("카드번호 패턴");
101
221
  if (RE_PHONE.test(v) && !hits.includes("전화번호 패턴"))
102
222
  hits.push("전화번호 패턴");
@@ -169,20 +289,21 @@ function makeEnumCheck(field, allowed) {
169
289
  }
170
290
  function makeDateYmdtCheck(fields) {
171
291
  return (obj) => {
172
- const present = fields.filter((f) => f in obj && typeof obj[f] === "string");
292
+ const present = fields.filter((f) => typeof pickField(obj, f) === "string");
173
293
  if (present.length === 0)
174
294
  return null;
175
- const fails = present.filter((f) => !/^\d{14}$/.test(obj[f]));
295
+ const fails = present.filter((f) => !/^\d{14}$/.test(pickField(obj, f)));
176
296
  return fails.length === 0
177
297
  ? { pass: true }
178
- : { pass: false, message: `YYYYMMDDHHmmss 14자리 필요: ${fails.map((f) => `${f}='${obj[f]}'`).join(", ")}` };
298
+ : { pass: false, message: `YYYYMMDDHHmmss 14자리 필요: ${fails.map((f) => `${f}='${pickField(obj, f)}'`).join(", ")}` };
179
299
  };
180
300
  }
181
301
  function makeDateYmdCheck(field, allowFull = false) {
182
302
  return (obj) => {
183
- if (!(field in obj) || typeof obj[field] !== "string")
303
+ const picked = pickField(obj, field);
304
+ if (typeof picked !== "string")
184
305
  return null;
185
- const val = obj[field];
306
+ const val = picked;
186
307
  const ok = /^\d{8}$/.test(val) || (allowFull && /^\d{14}$/.test(val));
187
308
  return ok
188
309
  ? { pass: true }
@@ -649,7 +770,7 @@ export function registerAllTools(server, store) {
649
770
  title: "API 요청 페이로드 자동 검증 + 검수 체크리스트",
650
771
  description: "PAYCO API 요청 페이로드(JSON)를 검증하고 서비스별 검수 체크리스트를 반환한다. " +
651
772
  "반환값은 반드시 사용자에게 전체 그대로 보여줄 것 — 요약하거나 생략하지 말 것. " +
652
- "체크리스트 항목은 ✓(자동통과) / ✗(자동실패, 이유 포함) / ⊘(필드 없어 스킵) / ⬜(수동확인 필요) 로 표시됨. " +
773
+ "체크리스트 항목은 ✓(통과) / ✗(수정 필요, 이유 포함) / ⊘(필드 없어 스킵) / ⬜(수동확인 필요) 로 표시됨. " +
653
774
  "⚠️ 운영 로그(실제 카드번호/주민번호/연락처)는 절대 그대로 붙여넣지 말 것.",
654
775
  inputSchema: {
655
776
  api: z
@@ -678,36 +799,53 @@ export function registerAllTools(server, store) {
678
799
  return notFound(`JSON 파싱 실패: ${e.message}`);
679
800
  }
680
801
  // --- API 명세 검증 (nested 평탄화 비교) ---
802
+ // extraData(JSON 문자열)는 검사용 사본(view)에서 풀어 안쪽까지 검사한다
803
+ const { view, issues: extraDataIssues } = expandExtraData(obj);
681
804
  const specByName = new Map(spec.request_params.map((p) => [p.name, p]));
682
- const flat = flattenPayload(obj);
805
+ const hasChildren = (name) => spec.request_params.some((q) => q.name.startsWith(name + "."));
806
+ const flat = flattenPayload(view);
683
807
  const missingRequired = [];
684
808
  const unknownParams = [];
685
- const typeMismatches = [];
686
- // 필수 누락: 명세 required 키가 평탄화된 payload 키에 없음
809
+ const typeMismatches = [...extraDataIssues];
810
+ // 필수 누락: 배열은 원소마다, 하위 항목은 상위를 보냈을 때만 확인
687
811
  for (const p of spec.request_params) {
688
- if (p.required && !flat.has(p.name)) {
689
- missingRequired.push(`${p.name} (${p.type}) — ${p.description.slice(0, 60)}`);
812
+ if (!p.required || !whenApplies(view, p.when))
813
+ continue;
814
+ for (const at of findMissingRequired(view, p.name)) {
815
+ missingRequired.push(`${at} (${p.type}) — ${p.description.slice(0, 60)}`);
690
816
  }
691
817
  }
692
818
  // 알 수 없는 파라미터: payload 의 평탄화 키 중 명세에 없는 것
819
+ // 하위 항목이 정의되지 않은 객체(예: 결제 취소의 extras)는 안쪽을 판단할 수 없으므로 명세 외로 보지 않는다
820
+ const underOpaqueObject = (key) => {
821
+ const parts = key.split(".");
822
+ for (let i = 1; i < parts.length; i++) {
823
+ const anc = parts.slice(0, i).join(".");
824
+ if (specByName.has(anc) && !hasChildren(anc))
825
+ return true;
826
+ }
827
+ return false;
828
+ };
693
829
  for (const key of flat.keys()) {
694
- if (!specByName.has(key))
830
+ if (!specByName.has(key) && !underOpaqueObject(key))
695
831
  unknownParams.push(key);
696
832
  }
697
833
  // 타입 불일치: 평탄화 키마다 명세 타입과 실제 값 타입 비교 (배열 안 객체는 동일 키로 여러 값)
698
834
  for (const [key, values] of flat.entries()) {
835
+ if (key === "extraData")
836
+ continue; // expandExtraData 에서 원본 기준으로 검사함
699
837
  const p = specByName.get(key);
700
838
  if (!p)
701
839
  continue;
702
840
  const expected = p.type.toLowerCase();
703
841
  for (const value of values) {
704
842
  const actual = Array.isArray(value) ? "array" : typeof value;
705
- const ok = (expected.includes("string") && actual === "string") ||
706
- (expected.includes("number") && actual === "number") ||
707
- (expected.includes("boolean") && actual === "boolean") ||
708
- (expected.includes("array") && actual === "array") ||
709
- (expected.includes("object") && actual === "object") ||
710
- expected === "unknown";
843
+ // 하위 항목이 정의된 항목은 가이드 표기가 string 이어도 객체/배열을 허용(예: extraData.viewOptions)
844
+ if ((actual === "object" || actual === "array") && hasChildren(key))
845
+ continue;
846
+ if (value === null)
847
+ continue; // null 은 보내지 않은 것으로 본다
848
+ const ok = typeMatches(expected, value) !== false;
711
849
  if (!ok) {
712
850
  typeMismatches.push(`${key}: 명세 '${p.type}' / 실제 '${actual}'`);
713
851
  break; // 같은 키의 다음 값들은 검사 스킵
@@ -720,7 +858,7 @@ export function registerAllTools(server, store) {
720
858
  const serviceLabel = SERVICE_LABELS[service] ?? service;
721
859
  const runResults = checklist.map((item) => ({
722
860
  item,
723
- result: item.autoCheck ? item.autoCheck(obj) : null,
861
+ result: item.autoCheck ? item.autoCheck(view) : null,
724
862
  }));
725
863
  const autoFailed = runResults.filter((r) => r.item.autoCheck && r.result !== null && !r.result.pass);
726
864
  const autoPassed = runResults.filter((r) => r.item.autoCheck && r.result !== null && r.result.pass);
@@ -787,7 +925,7 @@ export function registerAllTools(server, store) {
787
925
  // ── 헤더 ──────────────────────────────────────────────
788
926
  lines.push(`# ${pass ? "✓ PASS" : "✗ FAIL"} — ${spec.slug}`);
789
927
  lines.push(`[${spec.method ?? "?"}] ${spec.url ?? "(no url)"} | ${spec.title}`);
790
- lines.push(`서비스: ${serviceLabel} | 자동실패 ${checkFails + specFails}개`);
928
+ lines.push(`서비스: ${serviceLabel} | 수정 필요 ${checkFails + specFails}개`);
791
929
  lines.push("");
792
930
  // ── 검수 체크리스트 (항상 맨 앞에 전체 표시) ──────────
793
931
  lines.push(`## 검수 체크리스트 — ${serviceLabel} (${checklist.length}개)`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nhnpayco-dev/mcp-devcenter",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "PAYCO 개발자센터 가이드 전체(온라인·오프라인·오더·멤버십·결제정산) 를 AI 개발 도구에서 바로 조회·검색할 수 있게 해주는 MCP 서버 (PAYCO Developer Center integration guide MCP server for Claude, Cursor and AI agents)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,7 +29,7 @@
29
29
  "typescript": "^5.5.0"
30
30
  },
31
31
  "engines": {
32
- "node": ">=20"
32
+ "node": ">=24"
33
33
  },
34
34
  "keywords": [
35
35
  "mcp",