Skip to content

Commit 20299dc

Browse files
committed
fix: isuCd 클라이언트 필터링으로 단일 종목 조회 정상화
KRX API가 isuCd를 무시하고 전체 데이터를 반환하는 문제 대응: - API params에서 isuCd 제거, 클라이언트 측 ISU_CD 필터링 적용 - 캐시 키에서 isuCd 제외로 중복 캐시 방지 - MCP SDK 클라이언트 통합 테스트 추가 (isuCd → 1건 반환 검증)
1 parent a2a48a8 commit 20299dc

3 files changed

Lines changed: 46 additions & 12 deletions

File tree

src/cli/command-helper.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@ export async function executeCommand(
3737

3838
const parentOpts = program.opts();
3939

40-
const finalParams = parentOpts.code
41-
? { ...params, isuCd: parentOpts.code as string }
42-
: params;
40+
// isuCd is NOT sent to the API — KRX endpoints ignore it and return all rows.
41+
// Filtering is done client-side after fetch, which also avoids cache key duplication.
42+
const finalParams = params;
43+
44+
const codeFilter = parentOpts.code as string | undefined;
4345

4446
if (parentOpts.dryRun) {
4547
writeOutput(
@@ -48,6 +50,7 @@ export async function executeCommand(
4850
method: "POST",
4951
endpoint,
5052
params: finalParams,
53+
clientFilter: codeFilter ? { ISU_CD: codeFilter } : undefined,
5154
headers: { AUTH_KEY: "***" },
5255
},
5356
null,
@@ -130,6 +133,10 @@ export async function executeCommand(
130133
data = result.data as unknown as Record<string, unknown>[];
131134
}
132135

136+
if (codeFilter) {
137+
data = data.filter((row) => row["ISU_CD"] === codeFilter);
138+
}
139+
133140
if (data.length === 0) {
134141
writeError(noDataMessage ?? "No data");
135142
process.exit(EXIT_CODES.NO_DATA);

src/mcp/tools/index.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -175,17 +175,12 @@ function createCategoryTool(categoryId: CategoryId): ToolDefinition {
175175
);
176176
}
177177

178-
const extraParams: Record<string, string> = {};
179-
if (isuCd) {
180-
extraParams["isuCd"] = isuCd;
181-
}
182-
178+
// isuCd omitted from API params — see comment in single-date path above
183179
const rangeResult = await fetchDateRange({
184180
endpoint: endpoint.path,
185181
from: dateFrom,
186182
to: dateTo,
187183
apiKey,
188-
extraParams,
189184
});
190185

191186
if (!rangeResult.success) {
@@ -195,6 +190,10 @@ function createCategoryTool(categoryId: CategoryId): ToolDefinition {
195190
let data: readonly Record<string, string>[] =
196191
rangeResult.data as Record<string, string>[];
197192

193+
if (isuCd) {
194+
data = data.filter((row) => row["ISU_CD"] === isuCd);
195+
}
196+
198197
const filterExpr = args.filter as string | undefined;
199198
const sortField = args.sort as string | undefined;
200199
const sortDirection = (args.sort_direction as "asc" | "desc") ?? "desc";
@@ -228,10 +227,10 @@ function createCategoryTool(categoryId: CategoryId): ToolDefinition {
228227
);
229228
}
230229

230+
// isuCd is NOT passed to the API — KRX endpoints ignore it and return
231+
// all rows regardless. Client-side filtering is applied after fetch.
232+
// This also ensures a single cache entry per date (no isuCd in cache key).
231233
const params: Record<string, string> = { basDd: dateStr };
232-
if (isuCd) {
233-
params["isuCd"] = isuCd;
234-
}
235234

236235
const result = await krxFetch({
237236
endpoint: endpoint.path,
@@ -251,6 +250,10 @@ function createCategoryTool(categoryId: CategoryId): ToolDefinition {
251250

252251
let data: readonly Record<string, string>[] = result.data;
253252

253+
if (isuCd) {
254+
data = data.filter((row) => row["ISU_CD"] === isuCd);
255+
}
256+
254257
data = applyPipeline(data, {
255258
filter: filterExpr2,
256259
sort: sortField,

tests/mcp/mcp-truncation-e2e.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,4 +247,28 @@ describe("MCP 통합 테스트: truncation 시나리오", () => {
247247
const uniqueIds = new Set(collected.map((r) => r.ISU_CD));
248248
expect(uniqueIds.size).toBe(TOTAL_ROWS);
249249
});
250+
251+
it("isuCd로 특정 종목 조회 → 1건만 반환", async () => {
252+
handle = await startHttpServer({ port: 0, host: HOST });
253+
client = await createMcpClient(handle.port);
254+
255+
const targetIsuCd = FULL_DATA[42]!.ISU_CD;
256+
257+
const result = await client.callTool({
258+
name: "krx_stock",
259+
arguments: {
260+
endpoint: "stk_bydd_trd",
261+
date: "20260310",
262+
isuCd: targetIsuCd,
263+
},
264+
});
265+
266+
const text = (result.content as { type: string; text: string }[])[0]!.text;
267+
const parsed = JSON.parse(text) as Record<string, string>[];
268+
269+
expect(Array.isArray(parsed)).toBe(true);
270+
expect(parsed.length).toBe(1);
271+
expect(parsed[0]!.ISU_CD).toBe(targetIsuCd);
272+
expect(parsed[0]!.ISU_NM).toBe("테스트종목_0042");
273+
});
250274
});

0 commit comments

Comments
 (0)