objectiveai_sdk/client_objectiveai_mcp/server_request/payload.rs
1use indexmap::IndexMap;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4
5/// Tagged union of every JSON-RPC request the API forwards to the
6/// client over the reverse-attach WS. Variant names follow the same
7/// snake_case convention `client_request::Payload` uses; the
8/// `serde(tag = "type")` discriminator pairs with
9/// [`super::super::server_response::Payload`] by name.
10///
11/// MCP-routed variants carry `mcp_kind` directly on the variant
12/// (alongside the typed params via `#[serde(flatten)]`). The non-MCP
13/// `ReadMessageQueue` variant doesn't carry `mcp_kind` at all — it
14/// hits the CLI's own local state and never routes to an upstream
15/// MCP server. Use [`Payload::mcp_kind`] to retrieve it generically.
16#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
17#[schemars(rename = "client_objectiveai_mcp.server_request.Payload")]
18#[serde(tag = "type", rename_all = "snake_case")]
19pub enum Payload {
20 /// POST `initialize`. The proxy's `protocolVersion` doesn't ride
21 /// across this hop — the API discards it on the way in and
22 /// substitutes its own `canonical_initialize_result` on the way
23 /// out. The variant carries the plugin arguments the CLI needs at
24 /// dial time (parsed by the API off the URL query string).
25 #[schemars(title = "Initialize")]
26 Initialize {
27 mcp_kind: super::super::McpKind,
28 #[serde(flatten)]
29 params: InitializeRequest,
30 },
31
32 /// POST `tools/list`.
33 #[schemars(title = "ToolsList")]
34 ToolsList {
35 mcp_kind: super::super::McpKind,
36 #[serde(flatten)]
37 params: crate::mcp::tool::ListToolsRequest,
38 },
39
40 /// POST `tools/call`.
41 #[schemars(title = "ToolsCall")]
42 ToolsCall {
43 mcp_kind: super::super::McpKind,
44 #[serde(flatten)]
45 params: crate::mcp::tool::CallToolRequestParams,
46 },
47
48 /// POST `resources/list`.
49 #[schemars(title = "ResourcesList")]
50 ResourcesList {
51 mcp_kind: super::super::McpKind,
52 #[serde(flatten)]
53 params: crate::mcp::resource::ListResourcesRequest,
54 },
55
56 /// POST `resources/read`.
57 #[schemars(title = "ResourcesRead")]
58 ResourcesRead {
59 mcp_kind: super::super::McpKind,
60 #[serde(flatten)]
61 params: crate::mcp::resource::ReadResourceRequestParams,
62 },
63
64 /// `DELETE` on the routed MCP URL — the proxy closing the
65 /// session. No body beyond `mcp_kind`.
66 #[schemars(title = "SessionTerminate")]
67 SessionTerminate { mcp_kind: super::super::McpKind },
68
69 /// Read the CLI's local message queue for a given agent
70 /// hierarchy. Non-MCP — no `mcp_kind`. Non-destructive: the
71 /// API stamps the consumed ids onto the first
72 /// `AssistantResponseChunk.request_message_ids` it emits so
73 /// the downstream consumer owns row deletion; no separate
74 /// release RPC.
75 #[schemars(title = "ReadMessageQueue")]
76 ReadMessageQueue(ReadMessageQueueRequest),
77
78 /// Resolve a `Client` remote from the client's own local storage
79 /// (agent / swarm / function / profile). Non-MCP — no `mcp_kind`.
80 #[schemars(title = "Retrieve")]
81 Retrieve(super::super::retrieve::Request),
82
83 /// Forceful bulk teardown of every upstream connection for one
84 /// objectiveai response id. Non-MCP — no `mcp_kind` (it spans all
85 /// kinds for that response id). The CLI removes the whole
86 /// response-id bucket from its connection registry, which drops
87 /// every connection and kills every plugin subprocess under it.
88 /// Distinct from `SessionTerminate` (graceful per-`mcp_kind` MCP
89 /// `DELETE`); `Drop` is drop = kill.
90 #[schemars(title = "Drop")]
91 Drop(DropRequest),
92
93 /// Copy a file/folder from one laboratory to another (both on this
94 /// conduit). Non-MCP — it spans TWO laboratories, so it carries their
95 /// ids directly rather than a single `mcp_kind`. The conduit streams
96 /// the source laboratory's `/export` straight into the destination's
97 /// `/import` (a tar splice, no intermediate buffering).
98 #[schemars(title = "LaboratoryTransfer")]
99 LaboratoryTransfer(LaboratoryTransferRequest),
100}
101
102impl Payload {
103 /// Which CLI-hosted MCP server this payload targets. `Some` for
104 /// the MCP-routed variants; `None` for `ReadMessageQueue` which
105 /// hits the CLI's own local state.
106 pub fn mcp_kind(&self) -> Option<super::super::McpKind> {
107 match self {
108 Payload::Initialize { mcp_kind, .. }
109 | Payload::ToolsList { mcp_kind, .. }
110 | Payload::ToolsCall { mcp_kind, .. }
111 | Payload::ResourcesList { mcp_kind, .. }
112 | Payload::ResourcesRead { mcp_kind, .. }
113 | Payload::SessionTerminate { mcp_kind } => Some(mcp_kind.clone()),
114 Payload::ReadMessageQueue(_)
115 | Payload::Retrieve(_)
116 | Payload::Drop(_)
117 | Payload::LaboratoryTransfer(_) => None,
118 }
119 }
120}
121
122/// Parameters for [`Payload::ReadMessageQueue`].
123///
124/// Two-rule predicate (now that PENDING is gone):
125/// 1. Direct hit — `message_queue.agent_instance_hierarchy =
126/// agent_instance_hierarchy`.
127/// 2. BOUND-tag hit — `message_queue.agent_tag` resolves to a tag
128/// whose `tags.agent_instance_hierarchy = agent_instance_hierarchy`.
129///
130/// The conduit-side spawn-with-tag flow pre-fires the tag-group
131/// upgrade ahead of every read, so any tags sharing the spawn's
132/// group become BOUND before the EXISTS-check runs and feed
133/// straight into rule 2.
134///
135/// Returns rows oldest-first (`message_queue.id ASC`, which also
136/// matches `message_queue.enqueued_at` ascending due to
137/// AUTOINCREMENT). Row deletion is the downstream consumer's job:
138/// the API stamps the consumed ids onto the first emitted
139/// `AssistantResponseChunk.request_message_ids` so the consumer
140/// knows which rows it owns.
141#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
142#[schemars(rename = "client_objectiveai_mcp.server_request.ReadMessageQueueRequest")]
143pub struct ReadMessageQueueRequest {
144 pub agent_instance_hierarchy: String,
145}
146
147/// Parameters for [`Payload::Drop`]. The objectiveai response id whose
148/// entire upstream-connection bucket the CLI should tear down. The id
149/// rides in the payload (not a header) because `Drop` is a control
150/// message identified solely by its argument.
151#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
152#[schemars(rename = "client_objectiveai_mcp.server_request.DropRequest")]
153pub struct DropRequest {
154 pub response_id: String,
155}
156
157/// Parameters for [`Payload::LaboratoryTransfer`]. Copy `source_path` from
158/// the `source_id` laboratory into `dest_path` of the `dest_id` laboratory
159/// (cp-style: a directory `source_path` lands as `dest_path/<basename>`).
160#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
161#[schemars(rename = "client_objectiveai_mcp.server_request.LaboratoryTransferRequest")]
162pub struct LaboratoryTransferRequest {
163 pub source_id: String,
164 pub dest_id: String,
165 pub source_path: String,
166 pub dest_path: String,
167}
168
169/// Parameters for [`Payload::Initialize`].
170///
171/// Carries plugin arguments lifted off the inbound URL's query
172/// string (`?key=value&flag` → `{"key": Some("value"), "flag": None}`).
173/// Empty for [`super::super::McpKind::ObjectiveAi`] (the primary
174/// upstream takes no args).
175#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
176#[schemars(rename = "client_objectiveai_mcp.server_request.InitializeRequest")]
177pub struct InitializeRequest {
178 /// Plugin arguments the CLI passes through to
179 /// `<plugin> mcp <mcp_name> begin --<key> [value]`. `None` value
180 /// means presence-only flag (`--key`); `Some(v)` means `--key v`.
181 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
182 #[schemars(extend("omitempty" = true))]
183 pub args: IndexMap<String, Option<String>>,
184}