@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.
- package/README.md +14 -8
- package/dist/data/api-specs.json +1019 -220
- package/dist/data/index.json +1 -1
- package/dist/tools.js +162 -24
- package/package.json +2 -2
package/dist/data/index.json
CHANGED
|
@@ -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-
|
|
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
|
-
|
|
93
|
-
const
|
|
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 (
|
|
217
|
+
if (hasRrn(v) && !hits.includes("주민번호 패턴"))
|
|
98
218
|
hits.push("주민번호 패턴");
|
|
99
|
-
if (
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
-
|
|
303
|
+
const picked = pickField(obj, field);
|
|
304
|
+
if (typeof picked !== "string")
|
|
184
305
|
return null;
|
|
185
|
-
const val =
|
|
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
|
|
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
|
-
// 필수 누락:
|
|
809
|
+
const typeMismatches = [...extraDataIssues];
|
|
810
|
+
// 필수 누락: 배열은 원소마다, 하위 항목은 상위를 보냈을 때만 확인
|
|
687
811
|
for (const p of spec.request_params) {
|
|
688
|
-
if (p.required
|
|
689
|
-
|
|
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
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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(
|
|
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} |
|
|
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.
|
|
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": ">=
|
|
32
|
+
"node": ">=24"
|
|
33
33
|
},
|
|
34
34
|
"keywords": [
|
|
35
35
|
"mcp",
|