MCP 도구가 DB 조회까지 끝낸 뒤 BigInt 직렬화 오류로 실패한다면, 조회 실패와 응답 변환 실패를 먼저 분리한다. JavaScript의 BigInt 값을 그대로 JSON으로 보내려는 것이 원인일 수 있다. 큰 식별자는 무작정 Number로 바꾸지 말고, 필요한 필드만 십진 문자열로 변환한 뒤 출력 스키마와 받는 쪽의 계약을 함께 맞춘다.
적용 범위는 JavaScript·Node.js 기반 MCP 도구에서 결과 객체를 JSON으로 변환하는 경계다. 특정 DB 드라이버나 SDK의 장애를 확정하는 글은 아니다. 확인일은 2026년 10월 19일 한국시간, 합성 시험 환경은 Linux·Node.js v24.19.0이다. MCP 2026-07-28 명세와 ECMAScript 온라인 명세를 대조했으며, SDK 설치·실제 DB 연결·실제 MCP 통신 시험은 하지 않았다.
왜 작은 ID는 되는데 큰 ID에서 실패할까
먼저 값의 크기와 자료형을 구분한다. 기본 JSON.stringify는 1n처럼 작은 BigInt도 그대로 직렬화하지 못한다. 특정 큰 ID에서만 실패한다면 드라이버나 중간 변환이 언제 BigInt를 반환하는지 확인할 단서다. 모든 DB 드라이버가 같은 자료형을 반환한다고 가정하지 않는다.
ECMAScript의 SerializeJSONProperty 절차는 toJSON이나 replacer 처리를 거친 뒤에도 값이 BigInt이면 TypeError를 던지도록 정의한다. 따라서 DB 호출 성공 뒤 응답 객체를 직렬화하는 단계에서 예외가 생길 수 있다. 오류 문구 자체는 엔진에 따라 다르므로 메시지 한 줄보다 예외 자료형과 발생 단계를 확인한다.
가장 먼저 확인할 세 단계
1. 조회 함수가 결과를 반환했는지, 반환값을 JSON으로 만드는 단계에서 실패했는지 나눈다. 고객 원문이나 전체 행을 로그에 출력하지 말고, 합성 행으로 같은 변환 함수를 호출한다.
2. 필요한 필드의 typeof를 확인한다. 문자열로 받은 큰 ID와 BigInt로 받은 큰 ID는 같은 처리가 아니다. 이미 Number로 반올림된 뒤라면 뒤늦게 문자열로 바꿔도 원래 숫자는 복구되지 않는다.
3. 변환 뒤에는 직렬화 성공뿐 아니라 값 보존과 출력 계약을 확인한다. recordId는 이 글의 가상 업무 식별자이며 JSON-RPC 요청의 id와 다르다. 실제 응답의 요청 ID를 임의로 바꾸라는 뜻이 아니다.
Number로 바꾸면 오류는 사라져도 값이 바뀔 수 있다
ECMAScript의 Number.MAX_SAFE_INTEGER는 9,007,199,254,740,991이다. 이 범위를 벗어나는 모든 정수가 Number에서 서로 구별되는 것은 아니다. 아래 합성 값 9,007,199,254,740,993은 Number로 변환하면 9,007,199,254,740,992가 된다. 식별자가 다른 레코드를 가리킬 수 있으므로, 예외가 없어졌다는 것만으로 수정 성공을 판단하면 안 된다.
RFC 8259의 숫자 절도 구현별 범위·정밀도 한계와 상호운용 가능한 정수 범위를 설명한다. JSON에 긴 숫자 리터럴을 넣을 수 있다는 것과 받는 런타임이 그 값을 정확히 보존한다는 것은 별개다. 여기서는 계산용 수량이 아니라 정확한 동등성 비교가 필요한 식별자를 다룬다.
복사해서 실행하는 합성 시험
다음 코드를 bigint-probe.mjs에 저장하고 node bigint-probe.mjs로 실행한다. 외부 통신·패키지 설치·파일 변경은 없는 진단 코드다. 마지막 console.log는 독립 진단 프로그램용이며 MCP 프로토콜 stdout에 로그를 추가하라는 지시가 아니다.
import assert from 'node:assert/strict';
const source = { recordId: 9007199254740993n };
let errorName;
try { JSON.stringify(source); } catch (error) { errorName = error.name; }
assert.equal(errorName, 'TypeError');
const unsafe = Number(source.recordId);
assert.equal(String(unsafe), '9007199254740992');
assert.equal(Number.isSafeInteger(unsafe), false);
const wire = { recordId: source.recordId.toString() };
const encoded = JSON.stringify(wire);
const decoded = JSON.parse(encoded);
assert.equal(decoded.recordId, '9007199254740993');
assert.equal(BigInt(decoded.recordId), source.recordId);
console.log(JSON.stringify({ errorName, unsafe, safeInteger: Number.isSafeInteger(unsafe), encoded, roundTrip: true }));
실제 실행 출력은 다음과 같다. 기본 직렬화의 TypeError, Number 변환에서의 값 손실, 십진 문자열의 왕복 보존을 각각 확인했다. DB 장애나 MCP 종단 간 복구를 재현한 결과는 아니다.
{"errorName":"TypeError","unsafe":9007199254740992,"safeInteger":false,"encoded":"{\"recordId\":\"9007199254740993\"}","roundTrip":true}
수정은 전역 설정이 아니라 명시적인 반환 경계에서
이 예제에서는 반환할 recordId만 toString()으로 바꾼다. DB 행 전체를 그대로 반환하지 않으면 불필요한 필드 노출도 줄일 수 있다. 어떤 필드를 노출할지 정한 별도 응답 객체를 만들고, 큰 ID가 문자열이라는 계약을 문서화한다. 이미 드라이버가 문자열을 주는 경우에는 숫자로 바꾸었다가 되돌리는 과정을 추가하지 않는다.
모든 BigInt를 바꾸는 범용 replacer도 가능하지만, 수량·금액·식별자의 의미가 뒤섞인 객체에서는 자료형이 넓게 바뀔 수 있다. BigInt.prototype.toJSON을 전역으로 추가하는 방법은 다른 직렬화 호출에도 영향을 주므로 이 진단의 기본 처방으로 삼지 않는다. 문자열을 정수로 되돌릴 때도 합의된 필드에서만 형식과 허용 범위를 검사한 뒤 처리한다. 숫자로 보이는 모든 문자열을 자동 변환하면 선행 0이 중요한 코드 등을 훼손할 수 있다.
출력 스키마와 실제 응답을 함께 맞춘다
MCP 2026-07-28 기본 명세는 메시지가 JSON-RPC 2.0을 따르도록 요구한다. 같은 버전의 Tools 명세에서 outputSchema를 제공한 도구의 structuredContent는 그 스키마를 충족해야 한다. 직렬화만 통과시키려고 정수 필드를 문자열로 바꾸면 다음 단계에서 스키마 검증이 실패할 수 있다.
아래는 음수가 아닌 십진 식별자를 문자열로 주고받기로 합의한 경우의 가상 outputSchema 조각이다. 완전한 도구 정의나 실행한 SDK 예제가 아니다. 선행 0을 허용하지 않는 패턴이므로 원래 식별자 규칙이 다르면 그대로 적용하지 않는다. 최대 길이나 업무상 허용 범위는 별도 계약으로 정해야 한다.
{
"type": "object",
"properties": {
"recordId": {
"type": "string",
"pattern": "^(0|[1-9][0-9]*)$"
}
},
"required": [
"recordId"
],
"additionalProperties": false
}
기존 소비자가 정수를 기대한다면 서버만 바꾸어 배포하지 않는다. 호환성 점검, 소비자 수정, 필요하면 별도 버전의 도구 계약이 먼저다. 문자열이 됐다는 이유로 계산·정렬도 자동으로 올바르게 되는 것은 아니다. 숫자 크기 비교가 필요한 업무라면 합의한 범위 안에서 별도로 파싱하고 검증한다.
재호출하기 전에 확인할 것
조회 전용 도구와 쓰기 도구를 구분한다. DB 쓰기는 끝났지만 응답 직렬화만 실패한 경우라면 같은 호출을 반복해 중복 변경을 만들 수 있다. 응답을 못 받았다는 사실은 작업이 수행되지 않았다는 증거가 아니다. 승인된 읽기 경로로 실제 상태를 확인하고, 불확실하면 재실행 대신 확인 필요 상태로 남긴다.
적용 후에는 작은 BigInt, 큰 BigInt, 문자열 ID, 누락값을 나눠 시험한다. 각 경우의 허용 여부는 업무 계약으로 정한다. 이 글에서 실제 실행한 것은 위 큰 양의 합성 정수 한 건이며, 음수·배열·중첩 객체·DB 드라이버별 동작·SDK별 직렬화 경로는 검증하지 않았다.
연결해서 읽기
outputSchema와 structuredContent 검증 오류는 자료형을 바꾼 뒤의 계약 점검에 도움이 된다. isError와 JSON-RPC error 구분에서는 응답 오류와 업무 결과를 나눠 본다. 한글만 깨지는 문제라면 이번 직렬화 자료형 문제가 아니라 UTF-8 청크 경계 진단을 먼저 확인한다.