mirror of
https://github.com/openai/codex.git
synced 2026-05-02 04:11:39 +03:00
feat: replace custom mcp-types crate with equivalents from rmcp (#10349)
We started working with MCP in Codex before
https://crates.io/crates/rmcp was mature, so we had our own crate for
MCP types that was generated from the MCP schema:
8b95d3e082/codex-rs/mcp-types/README.md
Now that `rmcp` is more mature, it makes more sense to use their MCP
types in Rust, as they handle details (like the `_meta` field) that our
custom version ignored. Though one advantage that our custom types had
is that our generated types implemented `JsonSchema` and `ts_rs::TS`,
whereas the types in `rmcp` do not. As such, part of the work of this PR
is leveraging the adapters between `rmcp` types and the serializable
types that are API for us (app server and MCP) introduced in #10356.
Note this PR results in a number of changes to
`codex-rs/app-server-protocol/schema`, which merit special attention
during review. We must ensure that these changes are still
backwards-compatible, which is possible because we have:
```diff
- export type CallToolResult = { content: Array<ContentBlock>, isError?: boolean, structuredContent?: JsonValue, };
+ export type CallToolResult = { content: Array<JsonValue>, structuredContent?: JsonValue, isError?: boolean, _meta?: JsonValue, };
```
so `ContentBlock` has been replaced with the more general `JsonValue`.
Note that `ContentBlock` was defined as:
```typescript
export type ContentBlock = TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource;
```
so the deletion of those individual variants should not be a cause of
great concern.
Similarly, we have the following change in
`codex-rs/app-server-protocol/schema/typescript/Tool.ts`:
```
- export type Tool = { annotations?: ToolAnnotations, description?: string, inputSchema: ToolInputSchema, name: string, outputSchema?: ToolOutputSchema, title?: string, };
+ export type Tool = { name: string, title?: string, description?: string, inputSchema: JsonValue, outputSchema?: JsonValue, annotations?: JsonValue, icons?: Array<JsonValue>, _meta?: JsonValue, };
```
so:
- `annotations?: ToolAnnotations` ➡️ `JsonValue`
- `inputSchema: ToolInputSchema` ➡️ `JsonValue`
- `outputSchema?: ToolOutputSchema` ➡️ `JsonValue`
and two new fields: `icons?: Array<JsonValue>, _meta?: JsonValue`
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/10349).
* #10357
* __->__ #10349
* #10356
This commit is contained in:
@@ -2,6 +2,7 @@ use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use serde_json::Map;
|
||||
use serde_json::Value;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
@@ -92,7 +93,49 @@ fn read_file_bytes(path: &Path) -> Result<Vec<u8>> {
|
||||
|
||||
fn canonicalize_json(value: &Value) -> Value {
|
||||
match value {
|
||||
Value::Array(items) => Value::Array(items.iter().map(canonicalize_json).collect()),
|
||||
Value::Array(items) => {
|
||||
// NOTE: We sort some JSON arrays to make schema fixture comparisons stable across
|
||||
// platforms.
|
||||
//
|
||||
// In general, JSON array ordering is significant. However, this code path is used
|
||||
// only by `schema_fixtures_match_generated` to compare our *vendored* JSON schema
|
||||
// files against freshly generated output. Some parts of schema generation end up
|
||||
// with non-deterministic ordering across platforms (often due to map iteration order
|
||||
// upstream), which can cause Windows CI failures even when the generated schema is
|
||||
// semantically equivalent.
|
||||
//
|
||||
// JSON Schema itself also contains a number of array-valued keywords whose ordering
|
||||
// does not affect validation semantics (e.g. `required`, `type`, `enum`, `anyOf`,
|
||||
// `oneOf`, `allOf`). That makes it reasonable to treat many schema-emitted arrays as
|
||||
// order-insensitive for the purpose of fixture diffs.
|
||||
//
|
||||
// To avoid accidentally changing the meaning of arrays where order *could* matter
|
||||
// (e.g. tuple validation / `prefixItems`-style arrays), we only sort arrays when we
|
||||
// can derive a stable sort key for *every* element. If we cannot, we preserve the
|
||||
// original ordering.
|
||||
let items = items.iter().map(canonicalize_json).collect::<Vec<_>>();
|
||||
let mut sortable = Vec::with_capacity(items.len());
|
||||
for item in &items {
|
||||
let Some(key) = schema_array_item_sort_key(item) else {
|
||||
return Value::Array(items);
|
||||
};
|
||||
let stable = serde_json::to_string(item).unwrap_or_default();
|
||||
sortable.push((key, stable));
|
||||
}
|
||||
|
||||
let mut items = items.into_iter().zip(sortable).collect::<Vec<_>>();
|
||||
|
||||
items.sort_by(
|
||||
|(_, (key_left, stable_left)), (_, (key_right, stable_right))| match key_left
|
||||
.cmp(key_right)
|
||||
{
|
||||
Ordering::Equal => stable_left.cmp(stable_right),
|
||||
other => other,
|
||||
},
|
||||
);
|
||||
|
||||
Value::Array(items.into_iter().map(|(item, _)| item).collect())
|
||||
}
|
||||
Value::Object(map) => {
|
||||
let mut entries: Vec<_> = map.iter().collect();
|
||||
entries.sort_by(|(left, _), (right, _)| left.cmp(right));
|
||||
@@ -106,6 +149,25 @@ fn canonicalize_json(value: &Value) -> Value {
|
||||
}
|
||||
}
|
||||
|
||||
fn schema_array_item_sort_key(item: &Value) -> Option<String> {
|
||||
match item {
|
||||
Value::Null => Some("null".to_string()),
|
||||
Value::Bool(b) => Some(format!("b:{b}")),
|
||||
Value::Number(n) => Some(format!("n:{n}")),
|
||||
Value::String(s) => Some(format!("s:{s}")),
|
||||
Value::Object(map) => {
|
||||
if let Some(Value::String(reference)) = map.get("$ref") {
|
||||
Some(format!("ref:{reference}"))
|
||||
} else if let Some(Value::String(title)) = map.get("title") {
|
||||
Some(format!("title:{title}"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Value::Array(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_files_recursive(root: &Path) -> Result<BTreeMap<PathBuf, Vec<u8>>> {
|
||||
let mut files = BTreeMap::new();
|
||||
|
||||
@@ -146,3 +208,29 @@ fn collect_files_recursive(root: &Path) -> Result<BTreeMap<PathBuf, Vec<u8>>> {
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn canonicalize_json_sorts_string_arrays() {
|
||||
let value = serde_json::json!(["b", "a"]);
|
||||
let expected = serde_json::json!(["a", "b"]);
|
||||
assert_eq!(canonicalize_json(&value), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_json_sorts_schema_ref_arrays() {
|
||||
let value = serde_json::json!([
|
||||
{"$ref": "#/definitions/B"},
|
||||
{"$ref": "#/definitions/A"}
|
||||
]);
|
||||
let expected = serde_json::json!([
|
||||
{"$ref": "#/definitions/A"},
|
||||
{"$ref": "#/definitions/B"}
|
||||
]);
|
||||
assert_eq!(canonicalize_json(&value), expected);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user