mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +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:
@@ -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": {
|
||||
|
||||
+30
@@ -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",
|
||||
|
||||
+30
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+6
-1
@@ -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, };
|
||||
|
||||
@@ -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, };
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -116,6 +116,24 @@ pub struct McpResourceReadParams {
|
||||
pub uri: String,
|
||||
#[ts(optional = nullable)]
|
||||
pub connector_id: Option<String>,
|
||||
/// Explicit hosted app/account. Omit to retain legacy resource discovery.
|
||||
#[ts(optional = nullable)]
|
||||
pub target: Option<McpResourceReadTarget>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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<Vec<serde_json::Map<String, serde_json::Value>>>,
|
||||
list_resources: AtomicUsize,
|
||||
main_prompt_reads: AtomicUsize,
|
||||
reference_reads: AtomicUsize,
|
||||
@@ -1446,6 +1489,11 @@ impl ServerHandler for ResourceAppsMcpServer {
|
||||
request: ReadResourceRequestParams,
|
||||
context: RequestContext<RoleServer>,
|
||||
) -> Result<rmcp::model::ReadResourceResponse, rmcp::ErrorData> {
|
||||
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(
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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