mirror of
https://github.com/openai/codex.git
synced 2026-10-14 16:38:43 +00:00
Add explicit app and account targets to MCP resource reads (#47248)
## What changed
Allow `mcpServer/resource/read` to specify `target: {connectorId, linkId}` for direct hosted app reads without tool discovery, when supported by the backend. Require an explicit `linkId`: a string selects an account, while `null` requests no-auth access subject to backend policy.
Restrict direct targets to `codex_apps` and reject empty connector IDs, empty link IDs, and synthetic link IDs. Preserve originating app/account scope when `originCallId` and `threadId` are supplied, and retain discovery when `target` is omitted.
Update protocol schemas and TypeScript and Python models, preserving required-but-nullable `linkId` semantics in Python code generation.
## Testing
Extend resource-read tests to check target metadata with and without a thread, including explicit no-auth targets and omitted targets. Add Python model tests for string and null `linkId` values and rejection of a missing `linkId`.
GitOrigin-RevId: 8cf5595fbdd937193aeb529901fc95299f6e18ad
This commit is contained in:
@@ -576,7 +576,10 @@ def generate_v2_all(schema_dir: Path) -> None:
|
||||
cwd=sdk_root(),
|
||||
)
|
||||
_preserve_inline_image_class_names(out_path)
|
||||
_require_nullable_chatgpt_account_email(out_path)
|
||||
_require_nullable_field(out_path, "ChatgptAccount", r"email: str \| None")
|
||||
_require_nullable_field(
|
||||
out_path, "McpResourceReadTarget", r"link_id: Annotated\[\n(?: .*\n)+ \]"
|
||||
)
|
||||
_preserve_reasoning_effort_enum(out_path)
|
||||
_preserve_thread_source_enum(out_path)
|
||||
_preserve_plan_type_enum(out_path)
|
||||
@@ -601,27 +604,24 @@ def _preserve_inline_image_class_names(out_path: Path) -> None:
|
||||
out_path.write_text(source)
|
||||
|
||||
|
||||
def _require_nullable_chatgpt_account_email(out_path: Path) -> None:
|
||||
"""Preserve required-but-nullable email semantics in the generated SDK model."""
|
||||
def _require_nullable_field(out_path: Path, class_name: str, field_pattern: str) -> None:
|
||||
"""Preserve required-but-nullable fields that codegen gives a None default."""
|
||||
source = out_path.read_text()
|
||||
class_start = source.find("class ChatgptAccount(BaseModel):")
|
||||
class_start = source.find(f"class {class_name}(BaseModel):")
|
||||
if class_start == -1:
|
||||
raise RuntimeError("Generated SDK is missing ChatgptAccount")
|
||||
raise RuntimeError(f"Generated SDK is missing {class_name}")
|
||||
class_end = source.find("\n\nclass ", class_start)
|
||||
if class_end == -1:
|
||||
class_end = len(source)
|
||||
|
||||
class_source = source[class_start:class_end]
|
||||
nullable_with_default = " email: str | None = None"
|
||||
if class_source.count(nullable_with_default) != 1:
|
||||
raise RuntimeError(
|
||||
"Generated ChatgptAccount email did not have the expected nullable shape"
|
||||
)
|
||||
class_source = class_source.replace(
|
||||
nullable_with_default,
|
||||
" email: str | None",
|
||||
1,
|
||||
class_source, count = re.subn(
|
||||
rf"(^ {field_pattern}) = None$",
|
||||
r"\1",
|
||||
source[class_start:class_end],
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
if count != 1:
|
||||
raise RuntimeError(f"Generated {class_name} field did not have the expected nullable shape")
|
||||
out_path.write_text(source[:class_start] + class_source + source[class_end:])
|
||||
|
||||
|
||||
|
||||
@@ -2512,21 +2512,18 @@ class McpAuthStatus(Enum):
|
||||
o_auth = "oAuth"
|
||||
|
||||
|
||||
class McpResourceReadParams(BaseModel):
|
||||
class McpResourceReadTarget(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
)
|
||||
connector_id: Annotated[str | None, Field(alias="connectorId")] = None
|
||||
origin_call_id: Annotated[
|
||||
connector_id: Annotated[str, Field(alias="connectorId")]
|
||||
link_id: Annotated[
|
||||
str | None,
|
||||
Field(
|
||||
alias="originCallId",
|
||||
description="Originating MCP tool call used to select the resource's app.",
|
||||
alias="linkId",
|
||||
description="Null explicitly requests no-auth access, subject to the app's resource policy.",
|
||||
),
|
||||
] = None
|
||||
server: str
|
||||
thread_id: Annotated[str | None, Field(alias="threadId")] = None
|
||||
uri: str
|
||||
]
|
||||
|
||||
|
||||
class McpServerConnectionStatus(Enum):
|
||||
@@ -7380,17 +7377,6 @@ class ConfigMcpServerReloadRequest(BaseModel):
|
||||
params: None = None
|
||||
|
||||
|
||||
class McpServerResourceReadRequest(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
)
|
||||
id: RequestId
|
||||
method: Annotated[
|
||||
Literal["mcpServer/resource/read"], Field(title="McpServer/resource/readRequestMethod")
|
||||
]
|
||||
params: McpResourceReadParams
|
||||
|
||||
|
||||
class McpServerToolCallRequest(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
@@ -8492,6 +8478,27 @@ class LoginAccountParams(
|
||||
]
|
||||
|
||||
|
||||
class McpResourceReadParams(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
)
|
||||
connector_id: Annotated[str | None, Field(alias="connectorId")] = None
|
||||
origin_call_id: Annotated[
|
||||
str | None,
|
||||
Field(
|
||||
alias="originCallId",
|
||||
description="Originating MCP tool call used to select the resource's app.",
|
||||
),
|
||||
] = None
|
||||
server: str
|
||||
target: Annotated[
|
||||
McpResourceReadTarget | None,
|
||||
Field(description="Explicit hosted app/account. Omit to retain legacy resource discovery."),
|
||||
] = None
|
||||
thread_id: Annotated[str | None, Field(alias="threadId")] = None
|
||||
uri: str
|
||||
|
||||
|
||||
class McpResourceReadResponse(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
@@ -10473,6 +10480,17 @@ class McpServerStatusListRequest(BaseModel):
|
||||
params: ListMcpServerStatusParams
|
||||
|
||||
|
||||
class McpServerResourceReadRequest(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
)
|
||||
id: RequestId
|
||||
method: Annotated[
|
||||
Literal["mcpServer/resource/read"], Field(title="McpServer/resource/readRequestMethod")
|
||||
]
|
||||
params: McpResourceReadParams
|
||||
|
||||
|
||||
class AccountLoginStartRequest(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
|
||||
@@ -536,6 +536,18 @@ def test_generated_chatgpt_account_email_is_required_nullable() -> None:
|
||||
ChatgptAccount.model_validate({"planType": "pro", "type": "chatgpt"})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("link_id", ["link_calendar", None])
|
||||
def test_generated_resource_read_target_requires_explicit_account(link_id: str | None) -> None:
|
||||
from openai_codex.generated.v2_all import McpResourceReadTarget
|
||||
|
||||
payload = {"connectorId": "calendar", "linkId": link_id}
|
||||
target = McpResourceReadTarget.model_validate(payload)
|
||||
assert target.model_dump(by_alias=True) == payload
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
McpResourceReadTarget.model_validate({"connectorId": "calendar"})
|
||||
|
||||
|
||||
def test_generated_inline_image_class_names_remain_stable() -> None:
|
||||
"""Keep the existing Python class names when image references expand."""
|
||||
from openai_codex.generated.v2_all import (
|
||||
|
||||
Reference in New Issue
Block a user