1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
use rmcp::{
ServerHandler,
handler::server::router::tool::ToolRouter,
handler::server::tool::Extension,
handler::server::wrapper::Parameters,
model::{
Content, Implementation, ProtocolVersion, ServerCapabilities, ServerInfo,
},
schemars, tool, tool_handler, tool_router,
};
// --- Input schemas (matching Claude Code exactly) ---
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct BashRequest {
#[schemars(description = "The command to execute")]
command: String,
#[schemars(description = "Optional timeout in milliseconds (max 600000)")]
timeout: Option<u64>,
#[schemars(description = "Clear, concise description of what this command does in active voice")]
description: Option<String>,
}
// --- Tool server ---
#[derive(Debug, Clone)]
pub struct ObjectiveAiMcpLaboratory {
pub tool_router: ToolRouter<Self>,
shell_state: crate::bash::ShellState,
/// MCP server name reported in `get_info`:
/// `oail-<base62(fnv1a32(OBJECTIVEAI_LABORATORY_ID))>` — a fixed
/// 11-char hash token (the env value is the COMPOSITE laboratory
/// id `{machine}/{state}/{id}`; the raw id has arbitrary
/// length/charset and the composite carries a 64-hex machine id,
/// so NEITHER can ride the server name: it feeds the proxy's
/// tool-name prefix, which is bound by provider tool-name
/// limits). `oail` when run standalone.
server_name: String,
/// The full composite laboratory id, verbatim from the env —
/// surfaced via the server `instructions` so the assistant can
/// quote it (e.g. as `laboratory_transfer` source/destination).
/// `None` standalone or on a legacy (raw-id) env value.
composite_id: Option<String>,
}
#[tool_router]
impl ObjectiveAiMcpLaboratory {
pub fn new(laboratory_id: Option<String>, default_cwd: std::path::PathBuf) -> Self {
let (server_name, composite_id) = match laboratory_id {
Some(value) => {
// Hash whatever the env carries (the composite on
// every current container; a legacy raw id hashes the
// same way — uniformly name-safe either way). The
// composite is surfaced in instructions only when it
// actually parses.
let server_name = crate::composite::server_name(&value);
let composite_id =
crate::composite::parse_composite_laboratory_id(&value)
.map(|_| value);
(server_name, composite_id)
}
None => ("oail".to_string(), None),
};
let mut tool_router = Self::tool_router();
// Stamp the laboratory's FULL id into the Bash tool's
// description — descriptions are value-carrying (no provider
// name limits), so the composite rides verbatim; the static
// attribute text stays as the standalone/legacy fallback.
if let Some(composite) = &composite_id {
if let Some(route) = tool_router.map.get_mut("Bash") {
route.attr.description = Some(
format!(
"Executes a given command on laboratory {composite} and \
returns its output."
)
.into(),
);
}
}
Self {
tool_router,
shell_state: crate::bash::ShellState::new(default_cwd),
server_name,
composite_id,
}
}
/// Initialize session state (shell snapshot, etc.).
/// Should be called once after construction.
pub async fn init(&self) {
self.shell_state.init_snapshot().await;
}
/// The composite id is the laboratory's assistant-facing identity
/// — what `laboratory_transfer` sources/destinations and the
/// daemon's `/laboratories/{id}` routes are addressed by — so the
/// tool hands it over verbatim. Standalone runs (no laboratory)
/// and legacy raw-id containers have no composite to report and
/// say so in plain text rather than inventing one.
#[tool(
name = "get_laboratory_id",
description = "Returns the full id of this laboratory"
)]
async fn get_laboratory_id(&self) -> Content {
match &self.composite_id {
Some(composite) => Content::text(composite.clone()),
None => Content::text(
"this server is not running inside an identified laboratory",
),
}
}
#[tool(name = "Bash", description = "Executes a given bash command and returns its output.")]
async fn bash(
&self,
Parameters(req): Parameters<BashRequest>,
Extension(parts): Extension<http::request::Parts>,
) -> Content {
// The conduit forwards `X-OBJECTIVEAI-AGENT-INSTANCE-HIERARCHY` on every
// tools/call (1:1 with the agent instance). cwd/env are kept per-AIH so
// concurrent agents sharing this lab don't trample each other; a
// missing/empty header (standalone runs) routes to the `""` bucket.
// `HeaderMap::get` is case-insensitive.
let aih = parts
.headers
.get("x-objectiveai-agent-instance-hierarchy")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
match crate::bash::execute_bash(&self.shell_state, aih, &req.command, req.timeout).await {
Ok(output) => {
if output.is_image {
if let Some(parsed) = crate::bash::parse_data_uri(&output.stdout) {
return Content::image(parsed.data, parsed.media_type);
}
}
let json = serde_json::to_string_pretty(&output).unwrap_or_default();
Content::text(json)
}
Err(e) => Content::text(e),
}
}
}
#[tool_handler]
impl ServerHandler for ObjectiveAiMcpLaboratory {
fn get_info(&self) -> ServerInfo {
// rmcp 1.7 marks `ServerInfo`/`Implementation` `#[non_exhaustive]`,
// so build via `Default` + explicit field assignment.
let mut server_info = Implementation::default();
server_info.name = self.server_name.clone().into();
server_info.version = env!("CARGO_PKG_VERSION").into();
let mut info = ServerInfo::default();
info.protocol_version = ProtocolVersion::V_2025_06_18;
info.capabilities = ServerCapabilities::builder().enable_tools().build();
info.server_info = server_info;
// The assistant's in-band way to learn this laboratory's FULL
// id (machine/state/id — ids are only unique per (machine,
// state)): quote it verbatim, e.g. for `laboratory_transfer`.
info.instructions = self.composite_id.as_ref().map(|composite| {
format!(
"This laboratory's id is `{composite}`. Use it verbatim wherever \
a laboratory id is required (e.g. `laboratory_transfer` \
source/destination)."
)
});
info
}
}