diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 8b1f91ed5e..7a89eeaea7 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -2046,6 +2046,17 @@ "server": { "type": "string" }, + "target": { + "anyOf": [ + { + "$ref": "#/definitions/McpResourceReadTarget" + }, + { + "type": "null" + } + ], + "description": "Explicit hosted app/account. Omit to retain legacy resource discovery." + }, "threadId": { "type": [ "string", @@ -2062,6 +2073,25 @@ ], "type": "object" }, + "McpResourceReadTarget": { + "properties": { + "connectorId": { + "type": "string" + }, + "linkId": { + "description": "Null explicitly requests no-auth access, subject to the app's resource policy.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "connectorId", + "linkId" + ], + "type": "object" + }, "McpServerMigration": { "properties": { "name": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index f47d4929f6..6b1143f65e 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -14648,6 +14648,17 @@ "server": { "type": "string" }, + "target": { + "anyOf": [ + { + "$ref": "#/definitions/v2/McpResourceReadTarget" + }, + { + "type": "null" + } + ], + "description": "Explicit hosted app/account. Omit to retain legacy resource discovery." + }, "threadId": { "type": [ "string", @@ -14688,6 +14699,25 @@ "title": "McpResourceReadResponse", "type": "object" }, + "McpResourceReadTarget": { + "properties": { + "connectorId": { + "type": "string" + }, + "linkId": { + "description": "Null explicitly requests no-auth access, subject to the app's resource policy.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "connectorId", + "linkId" + ], + "type": "object" + }, "McpServerConnectionStatus": { "enum": [ "notStarted", diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index ea3d7fca29..b73fcc6bc6 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -10680,6 +10680,17 @@ "server": { "type": "string" }, + "target": { + "anyOf": [ + { + "$ref": "#/definitions/McpResourceReadTarget" + }, + { + "type": "null" + } + ], + "description": "Explicit hosted app/account. Omit to retain legacy resource discovery." + }, "threadId": { "type": [ "string", @@ -10720,6 +10731,25 @@ "title": "McpResourceReadResponse", "type": "object" }, + "McpResourceReadTarget": { + "properties": { + "connectorId": { + "type": "string" + }, + "linkId": { + "description": "Null explicitly requests no-auth access, subject to the app's resource policy.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "connectorId", + "linkId" + ], + "type": "object" + }, "McpServerConnectionStatus": { "enum": [ "notStarted", diff --git a/codex-rs/app-server-protocol/schema/json/v2/McpResourceReadParams.json b/codex-rs/app-server-protocol/schema/json/v2/McpResourceReadParams.json index ed20238ab4..0453605f62 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/McpResourceReadParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/McpResourceReadParams.json @@ -1,5 +1,26 @@ { "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "McpResourceReadTarget": { + "properties": { + "connectorId": { + "type": "string" + }, + "linkId": { + "description": "Null explicitly requests no-auth access, subject to the app's resource policy.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "connectorId", + "linkId" + ], + "type": "object" + } + }, "properties": { "connectorId": { "type": [ @@ -17,6 +38,17 @@ "server": { "type": "string" }, + "target": { + "anyOf": [ + { + "$ref": "#/definitions/McpResourceReadTarget" + }, + { + "type": "null" + } + ], + "description": "Explicit hosted app/account. Omit to retain legacy resource discovery." + }, "threadId": { "type": [ "string", diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index 9df82d48a8..934a5405c8 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index 5254e71a76..29c2b77ce6 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadParams.ts index 7e150661c8..d7fd4b36f5 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadParams.ts @@ -1,9 +1,14 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpResourceReadTarget } from "./McpResourceReadTarget"; export type McpResourceReadParams = { threadId?: string | null, /** * Originating MCP tool call used to select the resource's app. */ -originCallId?: string | null, server: string, uri: string, connectorId?: string | null, }; +originCallId?: string | null, server: string, uri: string, connectorId?: string | null, +/** + * Explicit hosted app/account. Omit to retain legacy resource discovery. + */ +target?: McpResourceReadTarget | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadTarget.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadTarget.ts new file mode 100644 index 0000000000..e112db2041 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadTarget.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpResourceReadTarget = { connectorId: string, +/** + * Null explicitly requests no-auth access, subject to the app's resource policy. + */ +linkId: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 514ae8de84..4cb9aa7ed2 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -283,6 +283,7 @@ export type { McpElicitationUntitledMultiSelectEnumSchema } from "./McpElicitati export type { McpElicitationUntitledSingleSelectEnumSchema } from "./McpElicitationUntitledSingleSelectEnumSchema"; export type { McpResourceReadParams } from "./McpResourceReadParams"; export type { McpResourceReadResponse } from "./McpResourceReadResponse"; +export type { McpResourceReadTarget } from "./McpResourceReadTarget"; export type { McpServerConnectionStatus } from "./McpServerConnectionStatus"; export type { McpServerElicitationAction } from "./McpServerElicitationAction"; export type { McpServerElicitationRequestParams } from "./McpServerElicitationRequestParams"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 8bd7d25da4..43a1a0071a 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -2469,6 +2469,7 @@ mod tests { server: "server-a".to_string(), uri: "file:///tmp/resource".to_string(), connector_id: None, + target: None, }, }; assert_eq!( @@ -2658,6 +2659,7 @@ mod tests { server: "server-a".to_string(), uri: "file:///tmp/resource".to_string(), connector_id: None, + target: None, }, }; assert_eq!(mcp_resource_read.serialization_scope(), None); diff --git a/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs b/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs index d1d920ddbd..55d6c8e86c 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs @@ -116,6 +116,24 @@ pub struct McpResourceReadParams { pub uri: String, #[ts(optional = nullable)] pub connector_id: Option, + /// Explicit hosted app/account. Omit to retain legacy resource discovery. + #[ts(optional = nullable)] + pub target: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpResourceReadTarget { + pub connector_id: String, + /// Null explicitly requests no-auth access, subject to the app's resource policy. + #[serde(deserialize_with = "Option::deserialize")] + #[schemars( + required, + schema_with = "crate::protocol::serde_helpers::nullable_string_schema" + )] + #[ts(type = "string | null")] + pub link_id: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 376d4c74bc..3971578823 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -66,6 +66,12 @@ stdio servers. The existing `mcp_2026_07_28` flag still governs eligible other servers, regardless of whether their names or URLs resemble hosted Apps. App-server does not persist this selection. +## Hosted resource reads + +`mcpServer/resource/read` accepts `target: {connectorId, linkId}` for direct hosted app reads without tool discovery. A string `linkId` selects that account; `null` explicitly requests no-auth access subject to backend policy. Do not infer no-auth access from unknown or synthetic links. + +`originCallId` with `threadId` takes precedence and retains the originating app/account scope. Requests without `target` retain discovery; `connectorId` continues to restrict reads to that connector. Direct targets require backend support for app/account resource reads. + # Project trust `thread/start` does not persist project trust for a directory where configuration diff --git a/codex-rs/app-server/src/request_processors/mcp_processor.rs b/codex-rs/app-server/src/request_processors/mcp_processor.rs index 9312b57beb..7e20110f8d 100644 --- a/codex-rs/app-server/src/request_processors/mcp_processor.rs +++ b/codex-rs/app-server/src/request_processors/mcp_processor.rs @@ -450,21 +450,41 @@ impl McpRequestProcessor { server, uri, connector_id, + target, } = params; let mut resource_params = ReadResourceRequestParams::new(uri); + let mut meta = serde_json::Map::new(); if let Some(connector_id) = connector_id { - resource_params.meta = Some( - serde_json::Map::from_iter([( - "x-codex-turn-metadata".to_string(), - serde_json::json!({ - "mcp_request_meta": { - "selected_connector_ids": [connector_id], - }, - }), - )]) - .into(), + meta.insert( + "x-codex-turn-metadata".to_string(), + serde_json::json!({ + "mcp_request_meta": {"selected_connector_ids": [connector_id]}, + }), ); } + if origin_call_id.is_none() + && let Some(target) = target + { + if server != codex_mcp::CODEX_APPS_MCP_SERVER_NAME + || target.connector_id.trim().is_empty() + || target + .link_id + .as_ref() + .is_some_and(|id| id.trim().is_empty() || id.starts_with("synthetic_link::")) + { + return Err(invalid_request( + "target requires codex_apps, a connectorId, and a real linkId or null", + )); + } + meta.insert( + "connector_id".to_string(), + serde_json::json!(target.connector_id), + ); + meta.insert("link_id".to_string(), serde_json::json!(target.link_id)); + } + if !meta.is_empty() { + resource_params.meta = Some(meta.into()); + } if let Some(thread_id) = thread_id { let (_, thread) = self.load_thread(&thread_id).await?; diff --git a/codex-rs/app-server/tests/suite/v2/mcp_resource.rs b/codex-rs/app-server/tests/suite/v2/mcp_resource.rs index c2b21c336a..e1b078fd17 100644 --- a/codex-rs/app-server/tests/suite/v2/mcp_resource.rs +++ b/codex-rs/app-server/tests/suite/v2/mcp_resource.rs @@ -1,6 +1,7 @@ use std::path::Path; use std::pin::pin; use std::sync::Arc; +use std::sync::Mutex; use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; @@ -28,6 +29,7 @@ use codex_app_server_protocol::InitializeParams; use codex_app_server_protocol::McpResourceContent; use codex_app_server_protocol::McpResourceReadParams; use codex_app_server_protocol::McpResourceReadResponse; +use codex_app_server_protocol::McpResourceReadTarget; use codex_app_server_protocol::McpServerToolCallParams; use codex_app_server_protocol::McpServerToolCallResponse; use codex_app_server_protocol::ProjectCreateParams; @@ -150,7 +152,7 @@ const SKILL_REFERENCE_CONTENTS: &str = #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn mcp_resource_read_returns_resource_contents() -> Result<()> { let responses_server = responses::start_mock_server().await; - let (apps_server_url, _apps_server_calls, apps_server_handle) = + let (apps_server_url, apps_server_calls, apps_server_handle) = start_resource_apps_mcp_server().await?; let responses_server_uri = responses_server.uri(); let (_codex_home, mut mcp) = start_resource_test_app_server( @@ -166,19 +168,54 @@ async fn mcp_resource_read_returns_resource_contents() -> Result<()> { ..Default::default() }) .await?; - let read_response: McpResourceReadResponse = mcp - .request(|request_id| ClientRequest::McpResourceRead { - request_id, - params: McpResourceReadParams { - thread_id: Some(thread.id), - origin_call_id: None, - server: "codex_apps".to_string(), - uri: TEST_RESOURCE_URI.to_string(), - connector_id: None, - }, - }) - .await?; - assert_eq!(read_response, expected_resource_read_response()); + for thread_id in [Some(thread.id), None] { + for target in [ + None, + Some(McpResourceReadTarget { + connector_id: "calendar".to_string(), + link_id: Some("link_calendar".to_string()), + }), + Some(McpResourceReadTarget { + connector_id: "calendar".to_string(), + link_id: None, + }), + ] { + let read_response: McpResourceReadResponse = mcp + .request(|request_id| ClientRequest::McpResourceRead { + request_id, + params: McpResourceReadParams { + thread_id: thread_id.clone(), + origin_call_id: None, + server: "codex_apps".to_string(), + uri: TEST_RESOURCE_URI.to_string(), + connector_id: None, + target: target.clone(), + }, + }) + .await?; + assert_eq!(read_response, expected_resource_read_response()); + let metadata = apps_server_calls + .read_metadata + .lock() + .expect("read metadata lock"); + let metadata = metadata + .last() + .expect("resource read reached the MCP server"); + assert_eq!( + ( + metadata.get("connector_id").cloned(), + metadata.get("link_id").cloned() + ), + match &target { + Some(target) => ( + Some(json!(target.connector_id)), + Some(json!(target.link_id)), + ), + None => (None, None), + }, + ); + } + } apps_server_handle.abort(); let _ = apps_server_handle.await; @@ -237,6 +274,7 @@ async fn mcp_resource_read_preserves_protocol_errors(protocol: ProtocolVersion) server: server.to_string(), uri: TEST_ERROR_RESOURCE_URI.to_string(), connector_id: None, + target: None, }) .await?; let error = timeout( @@ -452,6 +490,7 @@ apps = true server: "codex_apps".to_string(), uri: TEST_RESOURCE_URI.to_string(), connector_id: None, + target: None, }, }) .await?; @@ -465,6 +504,7 @@ apps = true server: "codex_apps".to_string(), uri: TEST_ELICITATION_RESOURCE_URI.to_string(), connector_id: None, + target: None, }, }) .await?; @@ -506,6 +546,7 @@ async fn mcp_resource_read_returns_error_for_unknown_thread() -> Result<()> { server: "codex_apps".to_string(), uri: TEST_RESOURCE_URI.to_string(), connector_id: None, + target: None, }, }) .await; @@ -630,6 +671,7 @@ async fn metadata_and_mcp_requests_complete_while_unrelated_resume_loads_config( server: "resource_server".to_string(), uri: TEST_RESOURCE_URI.to_string(), connector_id: None, + target: None, }, }), sender.request(ClientRequest::McpServerToolCall { @@ -1300,6 +1342,7 @@ fn expected_resource_read_response() -> McpResourceReadResponse { #[derive(Debug, Default)] pub(super) struct ResourceAppsMcpCalls { + read_metadata: Mutex>>, list_resources: AtomicUsize, main_prompt_reads: AtomicUsize, reference_reads: AtomicUsize, @@ -1446,6 +1489,11 @@ impl ServerHandler for ResourceAppsMcpServer { request: ReadResourceRequestParams, context: RequestContext, ) -> Result { + self.calls + .read_metadata + .lock() + .expect("read metadata lock") + .push(context.meta.0.0.clone()); let uri = request.uri; if uri == TEST_ERROR_RESOURCE_URI { return Err(rmcp::ErrorData::new( diff --git a/codex-rs/app-server/tests/suite/v2/mcp_resource/plugin_metadata_refresh.rs b/codex-rs/app-server/tests/suite/v2/mcp_resource/plugin_metadata_refresh.rs index 77ea32a5f9..daaff9ed9e 100644 --- a/codex-rs/app-server/tests/suite/v2/mcp_resource/plugin_metadata_refresh.rs +++ b/codex-rs/app-server/tests/suite/v2/mcp_resource/plugin_metadata_refresh.rs @@ -252,6 +252,7 @@ async fn read_mcp_skill_resource(app_server: &mut TestAppServer, thread_id: &str server: "codex_apps".to_string(), uri: SKILL_MAIN_PROMPT_URI.to_string(), connector_id: None, + target: None, }) .await?; let response: McpResourceReadResponse = diff --git a/codex-rs/app-server/tests/suite/v2/mcp_resource_origin.rs b/codex-rs/app-server/tests/suite/v2/mcp_resource_origin.rs index 49250930fa..32b04699bd 100644 --- a/codex-rs/app-server/tests/suite/v2/mcp_resource_origin.rs +++ b/codex-rs/app-server/tests/suite/v2/mcp_resource_origin.rs @@ -168,6 +168,7 @@ async fn widget_reads_survive_history_modes_compaction_restarts_and_app_only_vis server: "codex_apps".to_string(), uri: uri.to_string(), connector_id: None, + target: None, }) .await?; let error = timeout( @@ -291,6 +292,7 @@ async fn read_widget( server: "codex_apps".to_string(), uri: TEST_WIDGET_RESOURCE_URI.to_string(), connector_id: None, + target: None, }, }) .await diff --git a/sdk/python/scripts/update_sdk_artifacts.py b/sdk/python/scripts/update_sdk_artifacts.py index 4c36b595f0..635292b1cf 100755 --- a/sdk/python/scripts/update_sdk_artifacts.py +++ b/sdk/python/scripts/update_sdk_artifacts.py @@ -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:]) diff --git a/sdk/python/src/openai_codex/generated/v2_all.py b/sdk/python/src/openai_codex/generated/v2_all.py index 02bb318481..c167e3486c 100644 --- a/sdk/python/src/openai_codex/generated/v2_all.py +++ b/sdk/python/src/openai_codex/generated/v2_all.py @@ -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, diff --git a/sdk/python/tests/test_artifact_workflow_and_binaries.py b/sdk/python/tests/test_artifact_workflow_and_binaries.py index 1d2e23170b..039b0b86b2 100644 --- a/sdk/python/tests/test_artifact_workflow_and_binaries.py +++ b/sdk/python/tests/test_artifact_workflow_and_binaries.py @@ -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 (