같은 입력인데 MCP 도구의 형식 검사가 한 번은 통과하고 다음에는 실패한다면, 재사용하는 JavaScript 정규식에 g나 y 플래그가 있는지 먼저 확인한다. 기본 정규식 실행에서는 이 플래그가 검색 위치인 lastIndex를 상태로 사용한다. 단순히 맞는 형식인지 판별하는 검사라면, 전체 일치 규칙을 명시하고 불필요한 상태 플래그를 빼는 편이 진단하기 쉽다.
이 글은 MCP 도구 내부의 JavaScript 입력 검사에 관한 진단이다. MCP 프로토콜이나 특정 SDK의 오류를 재현한 글은 아니다. 아래 입력은 모두 합성 데이터이며, Node.js v24.19.0·V8 13.6.233.17-node.51에서 실행했다. 실제 호스트 연결, 사용자 데이터, 외부 API는 시험하지 않았다.
먼저 같은 객체로 네 번 검사한다
가상 상황으로, 작업 코드가 TASK-숫자 형식인지 확인하는 도구를 생각해 보자. 모듈 최상단에 정규식을 하나 만들고 요청마다 test를 호출하면 다음과 같은 현상이 나타날 수 있다. 새 정규식을 매번 만드는 시험만 하면 이 상태 공유를 놓친다.
const pattern = /^TASK-[0-9]+$/g;
for (let i = 0; i < 4; i++) {
const before = pattern.lastIndex;
const ok = pattern.test('TASK-12');
console.log({ before, ok, after: pattern.lastIndex });
}
// 0, true, 7
// 7, false, 0
// 0, true, 7
// 7, false, 0
위 결과는 이 입력과 패턴의 실제 합성 실행 결과다. 모든 전역 정규식이 무조건 true·false를 번갈아 반환한다는 뜻은 아니다. 문자열 길이, 일치 위치, 패턴과 호출 전 상태에 따라 결과는 달라진다. 이 사례에서는 첫 일치 후 위치가 7이 되고, 다음 검사는 그 위치에서 시작해 실패한 뒤 0으로 돌아간다.
목록 필터에서도 일부 항목만 빠진다
const pattern = /^TASK-[0-9]+$/g;
const rows = ['TASK-12', 'TASK-34', 'TASK-56'];
console.log(rows.filter(value => pattern.test(value)));
// ['TASK-12', 'TASK-56']
세 값 모두 기대 형식인데 가운데 값이 빠졌다. 여기에는 비동기 실행도 캐시 만료도 없다. 서로 다른 문자열을 검사해도 동일한 정규식 객체의 상태를 이어 쓰기 때문이다. 따라서 결과 누락을 곧바로 모델의 판단 오류나 검색 서버 문제로 해석하지 말고, 반환 직전의 필터 함수를 별도로 실행한다.
형식 판별과 연속 검색을 분리한다
다음은 “대문자 TASK-, ASCII 숫자 한 개 이상, 최대 64 UTF-16 코드 단위, 줄바꿈 없음”이라는 예시 계약의 판별 함수다. 64는 표준 제한이 아니라 이 가상 업무에서 정한 한도다. 실제 계약에 맞춰 변경해야 한다. 문자열이 아닌 입력을 조용히 문자열로 바꾸지 않는다.
const taskCode = /^TASK-[0-9]+$/; // g와 y 없음
const lineBreak = /[\r\n\u2028\u2029]/;
function isTaskCode(value) {
return typeof value === 'string' &&
value.length <= 64 &&
taskCode.test(value) &&
!lineBreak.test(value);
}
console.log(['TASK-12', 'TASK-34', 'TASK-56'].filter(isTaskCode));
// ['TASK-12', 'TASK-34', 'TASK-56']
console.log(isTaskCode('TASK-12\n')); // false
console.log(isTaskCode(null)); // false
줄바꿈을 따로 거부한 것은 입력 계약을 명시하기 위해서다. 앵커를 썼다는 사실만으로 공백·개행 정책까지 검토했다고 넘어가지 않는다. 반대로 공백을 허용해야 하는 업무에서 임의로 trim을 적용하면 원래 식별자와 다른 값을 승인할 수 있다. 정규화 여부 역시 업무 규칙으로 정한다.
어떤 수정을 고를까
| 목적 | 선택 | 주의할 점 |
|---|---|---|
| 입력 형식의 참·거짓 판별 | g·y 없는 전용 검사 | 전체 일치·타입·길이 계약도 확인 |
| 같은 문자열의 여러 일치 추출 | g 등 검색 상태 유지 | 한 탐색의 시작과 종료 책임 분리 |
| 정확한 현재 위치에서 토큰 읽기 | y와 명시적 위치 관리 | y를 빼면 원래 파서 의미가 바뀜 |
| 기존 상태 패턴을 독립 검사에 재사용 | 호출별 객체 또는 명시적 초기화 검토 | 다른 사용자의 탐색 상태를 덮어쓰지 않기 |
무조건 모든 정규식에서 g와 y를 지우는 일괄 수정은 피한다. 추출기나 토크나이저에서는 그 상태가 의도된 기능이다. lastIndex를 0으로 설정하는 처방도 공유 객체를 다른 코드가 탐색 중이면 그 진행 위치를 깨뜨린다. 형식 검사와 추출에 같은 객체를 돌려 쓰지 않는 것이 더 명확한 설계일 수 있다.
진단 순서와 회귀 검사
1. 문제를 일으킨 함수만 분리하고 같은 문자열을 같은 정규식 객체에 반복 입력한다. 호출 전후 lastIndex와 플래그를 함께 확인한다. 민감한 입력 원문은 로그에 남기지 말고 합성 문자열로 교체한다.
2. 새 객체를 매번 쓰는 경우와 같은 객체를 재사용하는 경우를 비교한다. 반복 검사는 안정적인데 실제 도구만 다르면, 입력 변환·다른 필터·요청별 상태·캐시 등 다음 경계를 조사한다. 정규식 원인으로 단정하지 않는다.
3. 정상 값 연속 입력, 서로 다른 정상 값, 정상·비정상 혼합, null·숫자·빈 문자열·줄바꿈을 나눠 검사한다. 배열 결과의 개수만 보지 말고 어떤 합성 항목이 빠졌는지도 대조한다.
4. 실제 배포 런타임에서 회귀 검사를 반복한다. 입력을 거부하는 동작과 도구의 오류 반환 계약을 연결하되, 실패를 빈 검색 결과로 숨기지 않는다. 이미 수행한 쓰기를 다시 실행하는 시험은 별도 상태 확인 없이 하지 않는다.
직접 확인한 범위와 남은 한계
2026년 10월 22일 Asia/Seoul 기준, 독립된 Node.js 스크립트에서 assertion 7개가 통과했다. 상태 패턴의 네 번 결과와 lastIndex, 필터의 가운데 항목 누락, 수정 함수의 정상 목록 보존, 다섯 비정상 입력 거부, 같은 정상 값 네 번 통과, y 패턴의 지정 위치 검색을 확인했다. 숫자나 null의 거부는 위 함수의 타입 검사 결과다.
이것은 작은 합성 입력의 로컬 검사다. 실제 MCP 서버에서 장애가 해결됐다는 증거도, 모든 정규식의 안전성 증명도 아니다. 사용자 입력으로 임의의 정규식을 생성하거나 복잡한 패턴을 쓰는 경우의 실행 시간 문제는 별도 검토가 필요하다. 여기서는 내장 동작을 바꾸지 않은 일반 RegExp를 전제로 하며, exec를 재정의한 객체는 범위 밖이다.
공식 근거와 관련 진단
공식 문서 확인일은 2026년 10월 22일 Asia/Seoul이다. TC39 RegExp.prototype.test는 RegExpExec의 일치 여부를 불리언으로 반환한다. RegExpBuiltinExec의 lastIndex·global·sticky 처리와 대조했다. 열람한 온라인 문서는 ECMAScript 2027 초안 표기다. 초안 전체의 구현을 주장하지 않으며, 예제 실행 버전은 위에 따로 명시했다.
배열 순서나 항목 값 자체가 바뀐다면 sort와 배열·객체 참조 진단으로 이어서 확인한다. 그 글은 원본 데이터 변경을 다루고, 이 글은 검사 도구 자체에 남는 검색 위치를 다룬다. 둘을 구분해야 캐시를 지우거나 모델 설정을 바꾸는 불필요한 수정을 피할 수 있다.