chore: support MCP schema 2025-06-18 (#1621)
This updates the schema in `generate_mcp_types.py` from `2025-03-26` to `2025-06-18`, regenerates `mcp-types/src/lib.rs`, and then updates all the code that uses `mcp-types` to honor the changes. Ran ``` npx @modelcontextprotocol/inspector just codex mcp ``` and verified that I was able to invoke the `codex` tool, as expected. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/1621). * #1623 * #1622 * __->__ #1621
This commit is contained in:
@@ -967,15 +967,17 @@ async fn run_task(sess: Arc<Session>, sub_id: String, input: Vec<InputItem>) {
|
||||
) => {
|
||||
items_to_record_in_conversation_history.push(item);
|
||||
let (content, success): (String, Option<bool>) = match result {
|
||||
Ok(CallToolResult { content, is_error }) => {
|
||||
match serde_json::to_string(content) {
|
||||
Ok(content) => (content, *is_error),
|
||||
Err(e) => {
|
||||
warn!("Failed to serialize MCP tool call output: {e}");
|
||||
(e.to_string(), Some(true))
|
||||
}
|
||||
Ok(CallToolResult {
|
||||
content,
|
||||
is_error,
|
||||
structured_content: _,
|
||||
}) => match serde_json::to_string(content) {
|
||||
Ok(content) => (content, *is_error),
|
||||
Err(e) => {
|
||||
warn!("Failed to serialize MCP tool call output: {e}");
|
||||
(e.to_string(), Some(true))
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => (e.clone(), Some(true)),
|
||||
};
|
||||
items_to_record_in_conversation_history.push(
|
||||
@@ -1353,7 +1355,7 @@ async fn handle_function_call(
|
||||
let params = match parse_container_exec_arguments(arguments, sess, &call_id) {
|
||||
Ok(params) => params,
|
||||
Err(output) => {
|
||||
return output;
|
||||
return *output;
|
||||
}
|
||||
};
|
||||
handle_container_exec_with_params(params, sess, sub_id, call_id).await
|
||||
@@ -1396,7 +1398,7 @@ fn parse_container_exec_arguments(
|
||||
arguments: String,
|
||||
sess: &Session,
|
||||
call_id: &str,
|
||||
) -> Result<ExecParams, ResponseInputItem> {
|
||||
) -> Result<ExecParams, Box<ResponseInputItem>> {
|
||||
// parse command
|
||||
match serde_json::from_str::<ShellToolCallParams>(&arguments) {
|
||||
Ok(shell_tool_call_params) => Ok(to_exec_params(shell_tool_call_params, sess)),
|
||||
@@ -1409,7 +1411,7 @@ fn parse_container_exec_arguments(
|
||||
success: None,
|
||||
},
|
||||
};
|
||||
Err(output)
|
||||
Err(Box::new(output))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,10 +135,12 @@ impl McpConnectionManager {
|
||||
experimental: None,
|
||||
roots: None,
|
||||
sampling: None,
|
||||
elicitation: None,
|
||||
},
|
||||
client_info: Implementation {
|
||||
name: "codex-mcp-client".to_owned(),
|
||||
version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
title: Some("Codex".into()),
|
||||
},
|
||||
protocol_version: mcp_types::MCP_SCHEMA_VERSION.to_owned(),
|
||||
};
|
||||
@@ -288,6 +290,8 @@ mod tests {
|
||||
r#type: "object".to_string(),
|
||||
},
|
||||
name: tool_name.to_string(),
|
||||
output_schema: None,
|
||||
title: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,10 +57,12 @@ async fn main() -> Result<()> {
|
||||
experimental: None,
|
||||
roots: None,
|
||||
sampling: None,
|
||||
elicitation: None,
|
||||
},
|
||||
client_info: Implementation {
|
||||
name: "codex-mcp-client".to_owned(),
|
||||
version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
title: Some("Codex".to_string()),
|
||||
},
|
||||
protocol_version: MCP_SCHEMA_VERSION.to_owned(),
|
||||
};
|
||||
|
||||
@@ -108,7 +108,10 @@ pub(crate) fn create_tool_for_codex_tool_call_param() -> Tool {
|
||||
|
||||
Tool {
|
||||
name: "codex".to_string(),
|
||||
title: Some("Codex".to_string()),
|
||||
input_schema: tool_input_schema,
|
||||
// TODO(mbolin): This should be defined.
|
||||
output_schema: None,
|
||||
description: Some(
|
||||
"Run a Codex session. Accepts configuration parameters matching the Codex Config struct.".to_string(),
|
||||
),
|
||||
@@ -179,6 +182,7 @@ mod tests {
|
||||
let tool_json = serde_json::to_value(&tool).expect("tool serializes");
|
||||
let expected_tool_json = serde_json::json!({
|
||||
"name": "codex",
|
||||
"title": "Codex",
|
||||
"description": "Run a Codex session. Accepts configuration parameters matching the Codex Config struct.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
|
||||
@@ -12,7 +12,7 @@ use codex_core::protocol::Op;
|
||||
use codex_core::protocol::Submission;
|
||||
use codex_core::protocol::TaskCompleteEvent;
|
||||
use mcp_types::CallToolResult;
|
||||
use mcp_types::CallToolResultContent;
|
||||
use mcp_types::ContentBlock;
|
||||
use mcp_types::JSONRPC_VERSION;
|
||||
use mcp_types::JSONRPCMessage;
|
||||
use mcp_types::JSONRPCResponse;
|
||||
@@ -44,12 +44,13 @@ pub async fn run_codex_tool_session(
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
let result = CallToolResult {
|
||||
content: vec![CallToolResultContent::TextContent(TextContent {
|
||||
content: vec![ContentBlock::TextContent(TextContent {
|
||||
r#type: "text".to_string(),
|
||||
text: format!("Failed to start Codex session: {e}"),
|
||||
annotations: None,
|
||||
})],
|
||||
is_error: Some(true),
|
||||
structured_content: None,
|
||||
};
|
||||
let _ = outgoing
|
||||
.send(JSONRPCMessage::Response(JSONRPCResponse {
|
||||
@@ -88,8 +89,6 @@ pub async fn run_codex_tool_session(
|
||||
tracing::error!("Failed to submit initial prompt: {e}");
|
||||
}
|
||||
|
||||
let mut last_agent_message: Option<String> = None;
|
||||
|
||||
// Stream events until the task needs to pause for user interaction or
|
||||
// completes.
|
||||
loop {
|
||||
@@ -98,17 +97,15 @@ pub async fn run_codex_tool_session(
|
||||
let _ = outgoing.send(codex_event_to_notification(&event)).await;
|
||||
|
||||
match &event.msg {
|
||||
EventMsg::AgentMessage(AgentMessageEvent { message }) => {
|
||||
last_agent_message = Some(message.clone());
|
||||
}
|
||||
EventMsg::ExecApprovalRequest(_) => {
|
||||
let result = CallToolResult {
|
||||
content: vec![CallToolResultContent::TextContent(TextContent {
|
||||
content: vec![ContentBlock::TextContent(TextContent {
|
||||
r#type: "text".to_string(),
|
||||
text: "EXEC_APPROVAL_REQUIRED".to_string(),
|
||||
annotations: None,
|
||||
})],
|
||||
is_error: None,
|
||||
structured_content: None,
|
||||
};
|
||||
let _ = outgoing
|
||||
.send(JSONRPCMessage::Response(JSONRPCResponse {
|
||||
@@ -121,12 +118,13 @@ pub async fn run_codex_tool_session(
|
||||
}
|
||||
EventMsg::ApplyPatchApprovalRequest(_) => {
|
||||
let result = CallToolResult {
|
||||
content: vec![CallToolResultContent::TextContent(TextContent {
|
||||
content: vec![ContentBlock::TextContent(TextContent {
|
||||
r#type: "text".to_string(),
|
||||
text: "PATCH_APPROVAL_REQUIRED".to_string(),
|
||||
annotations: None,
|
||||
})],
|
||||
is_error: None,
|
||||
structured_content: None,
|
||||
};
|
||||
let _ = outgoing
|
||||
.send(JSONRPCMessage::Response(JSONRPCResponse {
|
||||
@@ -137,27 +135,19 @@ pub async fn run_codex_tool_session(
|
||||
.await;
|
||||
break;
|
||||
}
|
||||
EventMsg::TaskComplete(TaskCompleteEvent {
|
||||
last_agent_message: _,
|
||||
}) => {
|
||||
let result = if let Some(msg) = last_agent_message {
|
||||
CallToolResult {
|
||||
content: vec![CallToolResultContent::TextContent(TextContent {
|
||||
r#type: "text".to_string(),
|
||||
text: msg,
|
||||
annotations: None,
|
||||
})],
|
||||
is_error: None,
|
||||
}
|
||||
} else {
|
||||
CallToolResult {
|
||||
content: vec![CallToolResultContent::TextContent(TextContent {
|
||||
r#type: "text".to_string(),
|
||||
text: String::new(),
|
||||
annotations: None,
|
||||
})],
|
||||
is_error: None,
|
||||
}
|
||||
EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message }) => {
|
||||
let text = match last_agent_message {
|
||||
Some(msg) => msg.clone(),
|
||||
None => "".to_string(),
|
||||
};
|
||||
let result = CallToolResult {
|
||||
content: vec![ContentBlock::TextContent(TextContent {
|
||||
r#type: "text".to_string(),
|
||||
text,
|
||||
annotations: None,
|
||||
})],
|
||||
is_error: None,
|
||||
structured_content: None,
|
||||
};
|
||||
let _ = outgoing
|
||||
.send(JSONRPCMessage::Response(JSONRPCResponse {
|
||||
@@ -177,6 +167,9 @@ pub async fn run_codex_tool_session(
|
||||
EventMsg::AgentReasoningDelta(_) => {
|
||||
// TODO: think how we want to support this in the MCP
|
||||
}
|
||||
EventMsg::AgentMessage(AgentMessageEvent { .. }) => {
|
||||
// TODO: think how we want to support this in the MCP
|
||||
}
|
||||
EventMsg::Error(_)
|
||||
| EventMsg::TaskStarted
|
||||
| EventMsg::TokenCount(_)
|
||||
@@ -200,12 +193,15 @@ pub async fn run_codex_tool_session(
|
||||
}
|
||||
Err(e) => {
|
||||
let result = CallToolResult {
|
||||
content: vec![CallToolResultContent::TextContent(TextContent {
|
||||
content: vec![ContentBlock::TextContent(TextContent {
|
||||
r#type: "text".to_string(),
|
||||
text: format!("Codex runtime error: {e}"),
|
||||
annotations: None,
|
||||
})],
|
||||
is_error: Some(true),
|
||||
// TODO(mbolin): Could present the error in a more
|
||||
// structured way.
|
||||
structured_content: None,
|
||||
};
|
||||
let _ = outgoing
|
||||
.send(JSONRPCMessage::Response(JSONRPCResponse {
|
||||
|
||||
@@ -70,9 +70,7 @@ pub async fn run_main(codex_linux_sandbox_exe: Option<PathBuf>) -> IoResult<()>
|
||||
JSONRPCMessage::Request(r) => processor.process_request(r),
|
||||
JSONRPCMessage::Response(r) => processor.process_response(r),
|
||||
JSONRPCMessage::Notification(n) => processor.process_notification(n),
|
||||
JSONRPCMessage::BatchRequest(b) => processor.process_batch_request(b),
|
||||
JSONRPCMessage::Error(e) => processor.process_error(e),
|
||||
JSONRPCMessage::BatchResponse(b) => processor.process_batch_response(b),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,9 @@ use crate::codex_tool_config::create_tool_for_codex_tool_call_param;
|
||||
use codex_core::config::Config as CodexConfig;
|
||||
use mcp_types::CallToolRequestParams;
|
||||
use mcp_types::CallToolResult;
|
||||
use mcp_types::CallToolResultContent;
|
||||
use mcp_types::ClientRequest;
|
||||
use mcp_types::ContentBlock;
|
||||
use mcp_types::JSONRPC_VERSION;
|
||||
use mcp_types::JSONRPCBatchRequest;
|
||||
use mcp_types::JSONRPCBatchResponse;
|
||||
use mcp_types::JSONRPCError;
|
||||
use mcp_types::JSONRPCErrorError;
|
||||
use mcp_types::JSONRPCMessage;
|
||||
@@ -145,41 +143,11 @@ impl MessageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a batch of requests and/or notifications.
|
||||
pub(crate) fn process_batch_request(&mut self, batch: JSONRPCBatchRequest) {
|
||||
tracing::info!("<- batch request containing {} item(s)", batch.len());
|
||||
for item in batch {
|
||||
match item {
|
||||
mcp_types::JSONRPCBatchRequestItem::JSONRPCRequest(req) => {
|
||||
self.process_request(req);
|
||||
}
|
||||
mcp_types::JSONRPCBatchRequestItem::JSONRPCNotification(note) => {
|
||||
self.process_notification(note);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle an error object received from the peer.
|
||||
pub(crate) fn process_error(&mut self, err: JSONRPCError) {
|
||||
tracing::error!("<- error: {:?}", err);
|
||||
}
|
||||
|
||||
/// Handle a batch of responses/errors.
|
||||
pub(crate) fn process_batch_response(&mut self, batch: JSONRPCBatchResponse) {
|
||||
tracing::info!("<- batch response containing {} item(s)", batch.len());
|
||||
for item in batch {
|
||||
match item {
|
||||
mcp_types::JSONRPCBatchResponseItem::JSONRPCResponse(resp) => {
|
||||
self.process_response(resp);
|
||||
}
|
||||
mcp_types::JSONRPCBatchResponseItem::JSONRPCError(err) => {
|
||||
self.process_error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_initialize(
|
||||
&mut self,
|
||||
id: RequestId,
|
||||
@@ -224,6 +192,7 @@ impl MessageProcessor {
|
||||
server_info: mcp_types::Implementation {
|
||||
name: "codex-mcp-server".to_string(),
|
||||
version: mcp_types::MCP_SCHEMA_VERSION.to_string(),
|
||||
title: Some("Codex".to_string()),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -333,12 +302,13 @@ impl MessageProcessor {
|
||||
if name != "codex" {
|
||||
// Tool not found – return error result so the LLM can react.
|
||||
let result = CallToolResult {
|
||||
content: vec![CallToolResultContent::TextContent(TextContent {
|
||||
content: vec![ContentBlock::TextContent(TextContent {
|
||||
r#type: "text".to_string(),
|
||||
text: format!("Unknown tool '{name}'"),
|
||||
annotations: None,
|
||||
})],
|
||||
is_error: Some(true),
|
||||
structured_content: None,
|
||||
};
|
||||
self.send_response::<mcp_types::CallToolRequest>(id, result);
|
||||
return;
|
||||
@@ -350,7 +320,7 @@ impl MessageProcessor {
|
||||
Ok(cfg) => cfg,
|
||||
Err(e) => {
|
||||
let result = CallToolResult {
|
||||
content: vec![CallToolResultContent::TextContent(TextContent {
|
||||
content: vec![ContentBlock::TextContent(TextContent {
|
||||
r#type: "text".to_owned(),
|
||||
text: format!(
|
||||
"Failed to load Codex configuration from overrides: {e}"
|
||||
@@ -358,6 +328,7 @@ impl MessageProcessor {
|
||||
annotations: None,
|
||||
})],
|
||||
is_error: Some(true),
|
||||
structured_content: None,
|
||||
};
|
||||
self.send_response::<mcp_types::CallToolRequest>(id, result);
|
||||
return;
|
||||
@@ -365,12 +336,13 @@ impl MessageProcessor {
|
||||
},
|
||||
Err(e) => {
|
||||
let result = CallToolResult {
|
||||
content: vec![CallToolResultContent::TextContent(TextContent {
|
||||
content: vec![ContentBlock::TextContent(TextContent {
|
||||
r#type: "text".to_owned(),
|
||||
text: format!("Failed to parse configuration for Codex tool: {e}"),
|
||||
annotations: None,
|
||||
})],
|
||||
is_error: Some(true),
|
||||
structured_content: None,
|
||||
};
|
||||
self.send_response::<mcp_types::CallToolRequest>(id, result);
|
||||
return;
|
||||
@@ -378,7 +350,7 @@ impl MessageProcessor {
|
||||
},
|
||||
None => {
|
||||
let result = CallToolResult {
|
||||
content: vec![CallToolResultContent::TextContent(TextContent {
|
||||
content: vec![ContentBlock::TextContent(TextContent {
|
||||
r#type: "text".to_string(),
|
||||
text:
|
||||
"Missing arguments for codex tool-call; the `prompt` field is required."
|
||||
@@ -386,6 +358,7 @@ impl MessageProcessor {
|
||||
annotations: None,
|
||||
})],
|
||||
is_error: Some(true),
|
||||
structured_content: None,
|
||||
};
|
||||
self.send_response::<mcp_types::CallToolRequest>(id, result);
|
||||
return;
|
||||
@@ -398,7 +371,7 @@ impl MessageProcessor {
|
||||
// Spawn an async task to handle the Codex session so that we do not
|
||||
// block the synchronous message-processing loop.
|
||||
task::spawn(async move {
|
||||
// Run the Codex session and stream events back to the client.
|
||||
// Run the Codex session and stream events Fck to the client.
|
||||
crate::codex_tool_runner::run_codex_tool_session(id, initial_prompt, config, outgoing)
|
||||
.await;
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Types for Model Context Protocol. Inspired by https://crates.io/crates/lsp-types.
|
||||
|
||||
As documented on https://modelcontextprotocol.io/specification/2025-03-26/basic:
|
||||
As documented on https://modelcontextprotocol.io/specification/2025-06-18/basic:
|
||||
|
||||
- TypeScript schema is the source of truth: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-03-26/schema.ts
|
||||
- JSON schema is amenable to automated tooling: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-03-26/schema.json
|
||||
- TypeScript schema is the source of truth: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-06-18/schema.ts
|
||||
- JSON schema is amenable to automated tooling: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-06-18/schema.json
|
||||
|
||||
@@ -14,7 +14,7 @@ from pathlib import Path
|
||||
# Helper first so it is defined when other functions call it.
|
||||
from typing import Any, Literal
|
||||
|
||||
SCHEMA_VERSION = "2025-03-26"
|
||||
SCHEMA_VERSION = "2025-06-18"
|
||||
JSONRPC_VERSION = "2.0"
|
||||
|
||||
STANDARD_DERIVE = "#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]\n"
|
||||
@@ -222,20 +222,7 @@ def add_definition(name: str, definition: dict[str, Any], out: list[str]) -> Non
|
||||
any_of = definition.get("anyOf", [])
|
||||
if any_of:
|
||||
assert isinstance(any_of, list)
|
||||
if name == "JSONRPCMessage":
|
||||
# Special case for JSONRPCMessage because its definition in the
|
||||
# JSON schema does not quite match how we think about this type
|
||||
# definition in Rust.
|
||||
deep_copied_any_of = json.loads(json.dumps(any_of))
|
||||
deep_copied_any_of[2] = {
|
||||
"$ref": "#/definitions/JSONRPCBatchRequest",
|
||||
}
|
||||
deep_copied_any_of[5] = {
|
||||
"$ref": "#/definitions/JSONRPCBatchResponse",
|
||||
}
|
||||
out.extend(define_any_of(name, deep_copied_any_of, description))
|
||||
else:
|
||||
out.extend(define_any_of(name, any_of, description))
|
||||
out.extend(define_any_of(name, any_of, description))
|
||||
return
|
||||
|
||||
type_prop = definition.get("type", None)
|
||||
@@ -609,6 +596,8 @@ def rust_prop_name(name: str, is_optional: bool) -> RustProp:
|
||||
prop_name = "r#type"
|
||||
elif name == "ref":
|
||||
prop_name = "r#ref"
|
||||
elif name == "enum":
|
||||
prop_name = "r#enum"
|
||||
elif snake_case := to_snake_case(name):
|
||||
prop_name = snake_case
|
||||
is_rename = True
|
||||
|
||||
2517
codex-rs/mcp-types/schema/2025-06-18/schema.json
Normal file
2517
codex-rs/mcp-types/schema/2025-06-18/schema.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@ use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
pub const MCP_SCHEMA_VERSION: &str = "2025-03-26";
|
||||
pub const MCP_SCHEMA_VERSION: &str = "2025-06-18";
|
||||
pub const JSONRPC_VERSION: &str = "2.0";
|
||||
|
||||
/// Paired request/response types for the Model Context Protocol (MCP).
|
||||
@@ -35,6 +35,12 @@ fn default_jsonrpc() -> String {
|
||||
pub struct Annotations {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub audience: Option<Vec<Role>>,
|
||||
#[serde(
|
||||
rename = "lastModified",
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub last_modified: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub priority: Option<f64>,
|
||||
}
|
||||
@@ -50,6 +56,14 @@ pub struct AudioContent {
|
||||
pub r#type: String, // &'static str = "audio"
|
||||
}
|
||||
|
||||
/// Base interface for metadata with name (identifier) and title (display name) properties.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct BaseMetadata {
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct BlobResourceContents {
|
||||
pub blob: String,
|
||||
@@ -58,6 +72,17 @@ pub struct BlobResourceContents {
|
||||
pub uri: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct BooleanSchema {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub default: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
pub r#type: String, // &'static str = "boolean"
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub enum CallToolRequest {}
|
||||
|
||||
@@ -75,29 +100,17 @@ pub struct CallToolRequestParams {
|
||||
}
|
||||
|
||||
/// The server's response to a tool call.
|
||||
///
|
||||
/// Any errors that originate from the tool SHOULD be reported inside the result
|
||||
/// object, with `isError` set to true, _not_ as an MCP protocol-level error
|
||||
/// response. Otherwise, the LLM would not be able to see that an error occurred
|
||||
/// and self-correct.
|
||||
///
|
||||
/// However, any errors in _finding_ the tool, an error indicating that the
|
||||
/// server does not support tool calls, or any other exceptional conditions,
|
||||
/// should be reported as an MCP error response.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct CallToolResult {
|
||||
pub content: Vec<CallToolResultContent>,
|
||||
pub content: Vec<ContentBlock>,
|
||||
#[serde(rename = "isError", default, skip_serializing_if = "Option::is_none")]
|
||||
pub is_error: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum CallToolResultContent {
|
||||
TextContent(TextContent),
|
||||
ImageContent(ImageContent),
|
||||
AudioContent(AudioContent),
|
||||
EmbeddedResource(EmbeddedResource),
|
||||
#[serde(
|
||||
rename = "structuredContent",
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub structured_content: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl From<CallToolResult> for serde_json::Value {
|
||||
@@ -127,6 +140,8 @@ pub struct CancelledNotificationParams {
|
||||
/// Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ClientCapabilities {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub elicitation: Option<serde_json::Value>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub experimental: Option<serde_json::Value>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -194,6 +209,7 @@ pub enum ClientResult {
|
||||
Result(Result),
|
||||
CreateMessageResult(CreateMessageResult),
|
||||
ListRootsResult(ListRootsResult),
|
||||
ElicitResult(ElicitResult),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
@@ -208,9 +224,18 @@ impl ModelContextProtocolRequest for CompleteRequest {
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct CompleteRequestParams {
|
||||
pub argument: CompleteRequestParamsArgument,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub context: Option<CompleteRequestParamsContext>,
|
||||
pub r#ref: CompleteRequestParamsRef,
|
||||
}
|
||||
|
||||
/// Additional, optional context for completions
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct CompleteRequestParamsContext {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub arguments: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// The argument's information
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct CompleteRequestParamsArgument {
|
||||
@@ -222,7 +247,7 @@ pub struct CompleteRequestParamsArgument {
|
||||
#[serde(untagged)]
|
||||
pub enum CompleteRequestParamsRef {
|
||||
PromptReference(PromptReference),
|
||||
ResourceReference(ResourceReference),
|
||||
ResourceTemplateReference(ResourceTemplateReference),
|
||||
}
|
||||
|
||||
/// The server's response to a completion/complete request
|
||||
@@ -248,6 +273,16 @@ impl From<CompleteResult> for serde_json::Value {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum ContentBlock {
|
||||
TextContent(TextContent),
|
||||
ImageContent(ImageContent),
|
||||
AudioContent(AudioContent),
|
||||
ResourceLink(ResourceLink),
|
||||
EmbeddedResource(EmbeddedResource),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub enum CreateMessageRequest {}
|
||||
|
||||
@@ -325,6 +360,48 @@ impl From<CreateMessageResult> for serde_json::Value {
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Cursor(String);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub enum ElicitRequest {}
|
||||
|
||||
impl ModelContextProtocolRequest for ElicitRequest {
|
||||
const METHOD: &'static str = "elicitation/create";
|
||||
type Params = ElicitRequestParams;
|
||||
type Result = ElicitResult;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ElicitRequestParams {
|
||||
pub message: String,
|
||||
#[serde(rename = "requestedSchema")]
|
||||
pub requested_schema: ElicitRequestParamsRequestedSchema,
|
||||
}
|
||||
|
||||
/// A restricted subset of JSON Schema.
|
||||
/// Only top-level properties are allowed, without nesting.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ElicitRequestParamsRequestedSchema {
|
||||
pub properties: serde_json::Value,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub required: Option<Vec<String>>,
|
||||
pub r#type: String, // &'static str = "object"
|
||||
}
|
||||
|
||||
/// The client's response to an elicitation request.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ElicitResult {
|
||||
pub action: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl From<ElicitResult> for serde_json::Value {
|
||||
fn from(value: ElicitResult) -> Self {
|
||||
// Leave this as it should never fail
|
||||
#[expect(clippy::unwrap_used)]
|
||||
serde_json::to_value(value).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// The contents of a resource, embedded into a prompt or tool call result.
|
||||
///
|
||||
/// It is up to the client how best to render embedded resources for the benefit
|
||||
@@ -346,6 +423,18 @@ pub enum EmbeddedResourceResource {
|
||||
|
||||
pub type EmptyResult = Result;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct EnumSchema {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
pub r#enum: Vec<String>,
|
||||
#[serde(rename = "enumNames", default, skip_serializing_if = "Option::is_none")]
|
||||
pub enum_names: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
pub r#type: String, // &'static str = "string"
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub enum GetPromptRequest {}
|
||||
|
||||
@@ -389,10 +478,12 @@ pub struct ImageContent {
|
||||
pub r#type: String, // &'static str = "image"
|
||||
}
|
||||
|
||||
/// Describes the name and version of an MCP implementation.
|
||||
/// Describes the name and version of an MCP implementation, with an optional title for UI representation.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Implementation {
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
@@ -442,24 +533,6 @@ impl ModelContextProtocolNotification for InitializedNotification {
|
||||
type Params = Option<serde_json::Value>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum JSONRPCBatchRequestItem {
|
||||
JSONRPCRequest(JSONRPCRequest),
|
||||
JSONRPCNotification(JSONRPCNotification),
|
||||
}
|
||||
|
||||
pub type JSONRPCBatchRequest = Vec<JSONRPCBatchRequestItem>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum JSONRPCBatchResponseItem {
|
||||
JSONRPCResponse(JSONRPCResponse),
|
||||
JSONRPCError(JSONRPCError),
|
||||
}
|
||||
|
||||
pub type JSONRPCBatchResponse = Vec<JSONRPCBatchResponseItem>;
|
||||
|
||||
/// A response to a request that indicates an error occurred.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct JSONRPCError {
|
||||
@@ -483,10 +556,8 @@ pub struct JSONRPCErrorError {
|
||||
pub enum JSONRPCMessage {
|
||||
Request(JSONRPCRequest),
|
||||
Notification(JSONRPCNotification),
|
||||
BatchRequest(JSONRPCBatchRequest),
|
||||
Response(JSONRPCResponse),
|
||||
Error(JSONRPCError),
|
||||
BatchResponse(JSONRPCBatchResponse),
|
||||
}
|
||||
|
||||
/// A notification which does not expect a response.
|
||||
@@ -777,6 +848,19 @@ pub struct Notification {
|
||||
pub params: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct NumberSchema {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub maximum: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub minimum: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
pub r#type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct PaginatedRequest {
|
||||
pub method: String,
|
||||
@@ -817,6 +901,17 @@ impl ModelContextProtocolRequest for PingRequest {
|
||||
type Result = Result;
|
||||
}
|
||||
|
||||
/// Restricted schema definitions that only allow primitive types
|
||||
/// without nested objects or arrays.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum PrimitiveSchemaDefinition {
|
||||
StringSchema(StringSchema),
|
||||
NumberSchema(NumberSchema),
|
||||
BooleanSchema(BooleanSchema),
|
||||
EnumSchema(EnumSchema),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub enum ProgressNotification {}
|
||||
|
||||
@@ -851,6 +946,8 @@ pub struct Prompt {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
/// Describes an argument that a prompt can accept.
|
||||
@@ -861,6 +958,8 @@ pub struct PromptArgument {
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub required: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
@@ -877,23 +976,16 @@ impl ModelContextProtocolNotification for PromptListChangedNotification {
|
||||
/// resources from the MCP server.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct PromptMessage {
|
||||
pub content: PromptMessageContent,
|
||||
pub content: ContentBlock,
|
||||
pub role: Role,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum PromptMessageContent {
|
||||
TextContent(TextContent),
|
||||
ImageContent(ImageContent),
|
||||
AudioContent(AudioContent),
|
||||
EmbeddedResource(EmbeddedResource),
|
||||
}
|
||||
|
||||
/// Identifies a prompt.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct PromptReference {
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
pub r#type: String, // &'static str = "ref/prompt"
|
||||
}
|
||||
|
||||
@@ -958,6 +1050,8 @@ pub struct Resource {
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub size: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
pub uri: String,
|
||||
}
|
||||
|
||||
@@ -969,6 +1063,26 @@ pub struct ResourceContents {
|
||||
pub uri: String,
|
||||
}
|
||||
|
||||
/// A resource that the server is capable of reading, included in a prompt or tool call result.
|
||||
///
|
||||
/// Note: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ResourceLink {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub annotations: Option<Annotations>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")]
|
||||
pub mime_type: Option<String>,
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub size: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
pub r#type: String, // &'static str = "resource_link"
|
||||
pub uri: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub enum ResourceListChangedNotification {}
|
||||
|
||||
@@ -977,13 +1091,6 @@ impl ModelContextProtocolNotification for ResourceListChangedNotification {
|
||||
type Params = Option<serde_json::Value>;
|
||||
}
|
||||
|
||||
/// A reference to a resource or resource template definition.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ResourceReference {
|
||||
pub r#type: String, // &'static str = "ref/resource"
|
||||
pub uri: String,
|
||||
}
|
||||
|
||||
/// A template description for resources available on the server.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ResourceTemplate {
|
||||
@@ -994,10 +1101,19 @@ pub struct ResourceTemplate {
|
||||
#[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")]
|
||||
pub mime_type: Option<String>,
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
#[serde(rename = "uriTemplate")]
|
||||
pub uri_template: String,
|
||||
}
|
||||
|
||||
/// A reference to a resource or resource template definition.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ResourceTemplateReference {
|
||||
pub r#type: String, // &'static str = "ref/resource"
|
||||
pub uri: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub enum ResourceUpdatedNotification {}
|
||||
|
||||
@@ -1140,6 +1256,7 @@ pub enum ServerRequest {
|
||||
PingRequest(PingRequest),
|
||||
CreateMessageRequest(CreateMessageRequest),
|
||||
ListRootsRequest(ListRootsRequest),
|
||||
ElicitRequest(ElicitRequest),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
@@ -1172,6 +1289,21 @@ pub struct SetLevelRequestParams {
|
||||
pub level: LoggingLevel,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct StringSchema {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub format: Option<String>,
|
||||
#[serde(rename = "maxLength", default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_length: Option<i64>,
|
||||
#[serde(rename = "minLength", default, skip_serializing_if = "Option::is_none")]
|
||||
pub min_length: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
pub r#type: String, // &'static str = "string"
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub enum SubscribeRequest {}
|
||||
|
||||
@@ -1213,6 +1345,25 @@ pub struct Tool {
|
||||
#[serde(rename = "inputSchema")]
|
||||
pub input_schema: ToolInputSchema,
|
||||
pub name: String,
|
||||
#[serde(
|
||||
rename = "outputSchema",
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub output_schema: Option<ToolOutputSchema>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
/// An optional JSON Schema object defining the structure of the tool's output returned in
|
||||
/// the structuredContent field of a CallToolResult.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ToolOutputSchema {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub properties: Option<serde_json::Value>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub required: Option<Vec<String>>,
|
||||
pub r#type: String, // &'static str = "object"
|
||||
}
|
||||
|
||||
/// A JSON Schema object defining the expected parameters for the tool.
|
||||
|
||||
@@ -17,8 +17,8 @@ fn deserialize_initialize_request() {
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"capabilities": {},
|
||||
"clientInfo": { "name": "acme-client", "version": "1.2.3" },
|
||||
"protocolVersion": "2025-03-26"
|
||||
"clientInfo": { "name": "acme-client", "title": "Acme", "version": "1.2.3" },
|
||||
"protocolVersion": "2025-06-18"
|
||||
}
|
||||
}"#;
|
||||
|
||||
@@ -37,8 +37,8 @@ fn deserialize_initialize_request() {
|
||||
method: "initialize".into(),
|
||||
params: Some(json!({
|
||||
"capabilities": {},
|
||||
"clientInfo": { "name": "acme-client", "version": "1.2.3" },
|
||||
"protocolVersion": "2025-03-26"
|
||||
"clientInfo": { "name": "acme-client", "title": "Acme", "version": "1.2.3" },
|
||||
"protocolVersion": "2025-06-18"
|
||||
})),
|
||||
};
|
||||
|
||||
@@ -57,12 +57,14 @@ fn deserialize_initialize_request() {
|
||||
experimental: None,
|
||||
roots: None,
|
||||
sampling: None,
|
||||
elicitation: None,
|
||||
},
|
||||
client_info: Implementation {
|
||||
name: "acme-client".into(),
|
||||
title: Some("Acme".to_string()),
|
||||
version: "1.2.3".into(),
|
||||
},
|
||||
protocol_version: "2025-03-26".into(),
|
||||
protocol_version: "2025-06-18".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use image::GenericImageView;
|
||||
use image::ImageReader;
|
||||
use lazy_static::lazy_static;
|
||||
use mcp_types::EmbeddedResourceResource;
|
||||
use mcp_types::ResourceLink;
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::style::Modifier;
|
||||
@@ -331,8 +332,7 @@ impl HistoryCell {
|
||||
) -> Option<Self> {
|
||||
match result {
|
||||
Ok(mcp_types::CallToolResult { content, .. }) => {
|
||||
if let Some(mcp_types::CallToolResultContent::ImageContent(image)) = content.first()
|
||||
{
|
||||
if let Some(mcp_types::ContentBlock::ImageContent(image)) = content.first() {
|
||||
let raw_data =
|
||||
match base64::engine::general_purpose::STANDARD.decode(&image.data) {
|
||||
Ok(data) => data,
|
||||
@@ -405,21 +405,21 @@ impl HistoryCell {
|
||||
|
||||
for tool_call_result in content {
|
||||
let line_text = match tool_call_result {
|
||||
mcp_types::CallToolResultContent::TextContent(text) => {
|
||||
mcp_types::ContentBlock::TextContent(text) => {
|
||||
format_and_truncate_tool_result(
|
||||
&text.text,
|
||||
TOOL_CALL_MAX_LINES,
|
||||
num_cols as usize,
|
||||
)
|
||||
}
|
||||
mcp_types::CallToolResultContent::ImageContent(_) => {
|
||||
mcp_types::ContentBlock::ImageContent(_) => {
|
||||
// TODO show images even if they're not the first result, will require a refactor of `CompletedMcpToolCall`
|
||||
"<image content>".to_string()
|
||||
}
|
||||
mcp_types::CallToolResultContent::AudioContent(_) => {
|
||||
mcp_types::ContentBlock::AudioContent(_) => {
|
||||
"<audio content>".to_string()
|
||||
}
|
||||
mcp_types::CallToolResultContent::EmbeddedResource(resource) => {
|
||||
mcp_types::ContentBlock::EmbeddedResource(resource) => {
|
||||
let uri = match resource.resource {
|
||||
EmbeddedResourceResource::TextResourceContents(text) => {
|
||||
text.uri
|
||||
@@ -430,6 +430,9 @@ impl HistoryCell {
|
||||
};
|
||||
format!("embedded resource: {uri}")
|
||||
}
|
||||
mcp_types::ContentBlock::ResourceLink(ResourceLink { uri, .. }) => {
|
||||
format!("link: {uri}")
|
||||
}
|
||||
};
|
||||
lines.push(Line::styled(line_text, Style::default().fg(Color::Gray)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user