[Bug]: Inconsistent structured output wrapping and opt-out for typing.Dict and typing sequence generics in func_metadata

Aberta Para iniciantes
#3,771 3 comentários 0 reações 0 responsáveis Ver no GitHub

Mantenedores costumam responder em até 1 dia

Ninguém assumiu esta issue ainda.

  • #3772 de @chauvuusvn — fechado sem integrar

Avaliação

Dificuldade
2/5
Tempo estimado
1-3 horas
Facilidade para iniciantes
82/100
Tipo de issue
Bug
Clareza
Claramente especificada
Status de atividade
Ativa
Stack de tecnologia
python
Domínio
api, backend

Direção de pesquisa

Comece por src/mcp/server/mcpserver/utilities/func_metadata.py e leia _create_output_model() e func_metadata(), onde a issue identifica as duas inconsistências. Confira os testes existentes de introspecção dos tipos de retorno e adicione cobertura para typing.Dict, typing.List e typing.Tuple sem argumentos de tipo, junto com seus equivalentes nativos. A tarefa estará concluída quando typing.Dict sem argumentos de tipo produzir um esquema de objeto sem wrapper e as sequências do módulo typing sem argumentos de tipo desativarem a saída estruturada conforme descrito.

Escrita pelo modelo de indexação a partir do texto da issue.

Descrição

bug v1 v2
Initial Checks
Release line

2.x (current stable)

Description

In src/mcp/server/mcpserver/utilities/func_metadata.py, return type introspection handles built-in types differently from their unsubscripted typing generic counterparts (typing.Dict, typing.List, typing.Tuple).

Issue 1: typing.Dict is forcibly wrapped in {"result": ...} instead of top-level object

When a tool returns dict or dict[str, Any], _create_output_model sets wrap_output = False and derives a top-level JSON object schema:

elif type_annotation is dict:
    model = dict[str, Any]

However, when a tool is annotated with typing.Dict (unsubscripted), get_origin(typing.Dict) is dict while get_args(typing.Dict) is (). In _create_output_model():

if origin is dict:
    args = get_args(type_expr)
    if len(args) == 2 and args[0] is str:
        model = type_expr
    else:
        # dict with non-str keys needs wrapping
        model = _create_wrapped_model(func_name, original_annotation)
        wrap_output = True

Because len(args) == 0 != 2, it falls through to else (intended only for non-str keys like dict[int, str]). As a result:

  • dict: treated as top-level JSON object (wrap_output = False)
  • typing.Dict: unexpectedly wrapped into {"result": ...} (wrap_output = True)
Issue 2: typing.List and typing.Tuple bypass the opt-out gate

In func_metadata():

if structured_output is None and (return_type_expr in (list, tuple) or _returns_content(return_type_expr)):
    return FuncMetadata(arg_model=arguments_model)

Because typing.List is not list (typing.List in (list, tuple) evaluates to False), tools annotated with -> typing.List or -> typing.Tuple fail to opt out and are forced through _create_output_model(), generating an unwanted wrapper schema { "result": typing.List }.

Example Code
import typing
from mcp.server.mcpserver.utilities.func_metadata import func_metadata

def tool_built_in_dict() -> dict:
    return {"status": "ok"}

def tool_typing_dict() -> typing.Dict:
    return {"status": "ok"}

def tool_built_in_list() -> list:
    return ["a", "b"]

def tool_typing_list() -> typing.List:
    return ["a", "b"]

meta_bi_dict = func_metadata(tool_built_in_dict)
meta_ty_dict = func_metadata(tool_typing_dict)
meta_bi_list = func_metadata(tool_built_in_list)
meta_ty_list = func_metadata(tool_typing_list)

# Inconsistent wrapping:
print("dict wrap_output:", meta_bi_dict.wrap_output)          # False
print("typing.Dict wrap_output:", meta_ty_dict.wrap_output)    # True (Expected: False)

# Inconsistent opt-out:
print("list schema:", meta_bi_list.output_schema)              # None
print("typing.List schema:", meta_ty_list.output_schema)        # {'type': 'object', 'properties': {'result': ...}} (Expected: None)
Proposed Fix
  1. In _create_output_model():
elif origin is dict:
    args = get_args(type_expr)
    if len(args) == 2 and args[0] is str:
        model = type_expr
    elif len(args) == 0:
        # Bare typing.Dict is equivalent to bare dict
        model = dict[str, Any]
    else:
        model = _create_wrapped_model(func_name, original_annotation)
        wrap_output = True
  1. In func_metadata():
if structured_output is None and (
    return_type_expr in (list, tuple, typing.List, typing.Tuple) or _returns_content(return_type_expr)
):
    return FuncMetadata(arg_model=arguments_model)
Version

Python 3.10+, MCP Python SDK 2.x

Linguagem predominante
Python
Estrelas
24.7k
Forks
4.1k
Merge médio
18h 27min
PRs com merge (30d)
15

Preparar o ambiente

Primeiros passos

  1. Leia a issue inteira e depois o guia de contribuição do projeto.
  2. Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
  3. Faça um fork do repositório e trabalhe em uma branch.
  4. Abra um pull request que referencie o número da issue.

Mais de modelcontextprotocol/python-sdk

Todas as issues de modelcontextprotocol/python-sdk

Issues semelhantes

Mais issues de Python

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.