MCP를 쓰지 않았는데 mcp extra가 필요했다: Pydantic AI 2.43의 연산 식별 수정
1. 평범한 함수 도구가 선택 패키지 하나 때문에 멈췄다
Pydantic AI에서 Temporal durable execution을 쓰되 특정 도구만 activity 밖에서 실행하고 싶다면 도구 메타데이터에 temporal: False를 줄 수 있다. 비동기 함수 도구처럼 workflow 안에서 안전하게 실행할 수 있는 작업은 이 경로를 사용할 수 있다. 그런데 2.42.0에서는 MCP를 전혀 쓰지 않은 평범한 함수 도구도 선택 패키지인 mcp extra가 없으면 ImportError로 멈추는 사례가 나왔다.
이슈 #8249의 재현 환경은 Python 3.11.15와 pydantic-ai-slim[temporal] 2.42.0이다. 내장 TestModel과 함수 도구만 사용하므로 외부 모델 SDK도 필요 없다. 문제는 도구가 실행된 뒤가 아니라, activity opt-out 설정을 해석하는 도중 발생했다. 사용자가 선택하지 않은 기능의 import가 공통 분기 안에 숨어 있었던 셈이다.
이 버그는 단순한 의존성 누락으로만 보면 놓치는 부분이 있다. mcp extra를 추가하면 첫 증상은 사라지지만, 실행 경계의 종류를 Python 객체 타입으로 판정하는 설계는 그대로 남는다. Pydantic AI 2.43.0은 import를 조건부로 감싸는 대신, durable operation을 만들 때 이미 기록한 toolset_kind를 사용하도록 바꿨다.
그림 1. 현재 도구가 MCP인지 확인하기 전에 선택 기능 import가 먼저 실행돼 일반 함수 경로까지 막았다. 출처: github.com/pydantic/pydantic-ai/blob/v2.42.0/pydantic_ai_slim/pydantic_ai/durable_exec/temporal/_durability.py.
2. 2.42.0은 왜 모든 opt-out 도구에서 MCP를 불러왔나
2.42.0의 _resolve_temporal_tool_config()는 먼저 도구별 activity 설정을 계산한다. 결과가 False이면 activity를 사용하지 않겠다는 뜻이다. 바로 이 분기 첫 줄에 from pydantic_ai.mcp import MCPToolset이 있었다. 뒤에서 현재 도구가 MCP인지 검사하려고 불러온 타입이지만, Python import는 검사보다 먼저 실행된다.
따라서 현재 도구가 일반 FunctionToolset에 속하더라도 mcp extra가 없으면 분기 초입에서 실패한다. 코드가 알고 싶었던 것은 "이 연산이 MCP 도구 호출인가"였지만, 확인 방식 때문에 "MCP 모듈을 지금 import할 수 있는가"가 선행 조건이 됐다. 기능 판정과 설치 상태가 뜻하지 않게 묶였다.
단순한 try/except ImportError는 첫 사례를 막을 수 있다. 실제로 PR 설명에는 이전 시도 #8255가 이런 접근을 썼다고 적혀 있다. 하지만 그러면 두 번째 문제가 남는다. 도구를 감싸는 wrapper가 있으면 현재 보이는 객체 타입과 실제 durable operation의 종류가 달라질 수 있다.
3. wrapper는 객체의 겉모양을 바꾸지만 연산의 기원은 바꾸지 않는다
PrefixedToolset은 도구 이름 앞에 접두사를 붙이는 wrapper다. 이 wrapper를 통과한 MCP 도구의 tool.toolset은 원래 MCPToolset이 아니라 wrapper를 가리킬 수 있다. 2.42.0의 isinstance(tool.toolset, MCPToolset) 검사는 이런 도구를 MCP로 알아보지 못한다. 이어지는 함수 도구 전용 가정에서 AssertionError가 날 수 있었다.
반면 durable operation ID에는 도구 경로를 만들 때 toolset_kind가 기록된다. 일반 함수 도구는 function, MCP 도구는 mcp, 동적 도구 집합은 dynamic으로 식별된다. wrapper가 표시용 이름이나 노출 객체를 바꿔도 이 연산 종류는 남는다.
PR #8257은 opt-out 분기에서 먼저 현재 ID가 tool operation인지 확인한 뒤 operation_id.toolset_kind를 읽는다. dynamic은 workflow 안에서 도구 해석이나 호출 중 I/O가 생길 수 있어 거부하고, mcp 역시 I/O가 필요하므로 거부한다. 일반 함수 도구 경로에서는 MCP 모듈을 import하지 않는다.
그림 2. 2.43.0은 wrapper 뒤의 객체 타입 대신 생성 시 기록한 operation kind로 실행 정책을 판정한다. 출처: github.com/pydantic/pydantic-ai/pull/8257.
4. 이 수정이 지키는 Temporal의 경계
Temporal workflow는 같은 입력과 기록된 이력으로 다시 실행할 때 같은 결정을 내야 한다. 네트워크 요청처럼 반복할 때 결과가 달라질 수 있는 작업은 activity로 분리하는 것이 기본 구조다. Pydantic AI의 Temporal 문서도 workflow를 결정적 부분, activity를 I/O와 비결정적 작업을 맡는 부분으로 설명한다.
그래서 모든 도구에 temporal: False를 허용하는 것이 수정의 목적은 아니다. MCP와 dynamic toolset은 여전히 workflow 내부 실행이 금지된다. 달라진 것은 금지 대상을 찾는 기준이다. 선택 패키지의 타입을 import해 현재 객체를 검사하지 않고, durable layer가 이미 알고 있는 operation kind로 경계를 판정한다.
이 구분은 오류 메시지에도 영향을 준다. 평범한 비동기 함수 도구는 불필요한 ImportError 없이 opt-out을 해석할 수 있다. 반대로 MCP 도구는 wrapper 안에 있어도 toolset_kind == 'mcp'로 잡혀, I/O 도구는 workflow 밖에서 실행할 수 없다는 UserError를 받는다. 잘못된 설치 오류나 내부 assertion보다 운영자가 고칠 수 있는 실패에 가깝다.
5. 두 회귀 테스트가 각각 다른 실패를 막는다
2.43.0 태그의 테스트에는 두 사례가 추가됐다. 첫 테스트는 sys.modules['pydantic_ai.mcp'] = None으로 MCP 모듈을 불러올 수 없는 설치를 흉내 낸다. 일반 비동기 함수 도구에 temporal: False를 주고 설정 해석 결과가 False인지 확인한다. 핵심은 MCP 없이도 MCP와 무관한 경로가 작동한다는 점이다.
두 번째 테스트는 MCP 도구를 PrefixedToolset으로 감싼다. 외부에서 보이는 tool.toolset이 wrapper로 바뀌어도 operation ID에는 mcp가 남는다. 테스트는 이 경로가 AssertionError로 빠지지 않고 "MCP tools require the use of IO"라는 UserError를 내는지 확인한다.
이번 검토에서는 2.42.0과 2.43.0 태그의 _durability.py를 직접 비교했다. 2.42.0에는 opt-out 분기 안의 MCP import와 객체 타입 검사가 있고, 2.43.0에는 그 import가 없으며 is_tool_operation과 두 toolset_kind 비교가 들어 있다. 또한 2.43.0 태그에서 위 두 테스트를 대상으로 실행해 통과 여부를 별도 확인했다. 이는 해당 회귀 경로를 확인한 것이지 Temporal 전체 동작이나 모든 wrapper 조합을 검증한 것은 아니다.
그림 3. 회귀 테스트는 허용해야 할 함수 도구와 계속 거부해야 할 wrapped MCP를 각각 고정한다. 출처: github.com/pydantic/pydantic-ai/blob/v2.43.0/tests/durable_exec/temporal/test_durability.py.
6. durable agent 통합에서 바로 확인할 것
선택 의존성이 공통 실행 경로에 섞였는지 먼저 본다. foo extra를 쓰지 않는 사용자가 관련 모듈을 설치하지 않은 상태에서도 기본 도구, 검증, 오류 처리 경로가 돌아가야 한다. 테스트 환경에 모든 extra를 한꺼번에 설치하면 이런 결함이 숨어 버린다. 최소 설치 조합을 별도 CI 축으로 두는 편이 낫다.
실행 종류는 wrapper 바깥의 현재 객체보다 생성 시점의 명시적 식별자로 판정한다. 이름 접두사, 캐시, 권한 wrapper가 겹치면 객체 그래프는 바뀐다. 반면 function, mcp, dynamic처럼 operation에 붙인 종류는 실행 정책이 참고할 안정적인 값이 된다. 다만 이 값도 생성 단계에서 잘못 붙이면 안전하지 않으므로 operation 생성과 정책 판정을 함께 테스트해야 한다.
마지막으로 거부 경로를 성공 경로만큼 구체적으로 검증한다. I/O가 필요한 MCP 도구를 workflow 안에서 실행하도록 풀어 버리면 안 된다. 이번 수정은 제한을 완화하지 않고, 일반 함수 도구의 불필요한 실패와 wrapped MCP의 누락을 동시에 고쳤다. 의존성 오류를 없앴다는 한 문장보다, 어떤 연산은 통과하고 어떤 연산은 계속 거부되는지를 보는 편이 정확하다.



댓글
댓글 쓰기