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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
//! Model Context Protocol (MCP) integration: connect to an MCP server over
//! stdio (a spawned child process) or streamable HTTP (a remote server by URL),
//! and surface each tool it reports as a [`DynTool`](crate::DynTool) the runtime
//! dispatches through like any native tool.
//!
//! This whole module sits behind the `mcp` cargo feature. Everything MCP lives
//! here and nowhere else in the workspace: the rmcp SDK, the Tokio runtime it
//! needs, and the mapping from MCP's wire types to this crate's tool contract.
//! That isolation is deliberate. rmcp/MCP protocol churn is a
//! standing risk, and the mitigation is exactly this: one module, one
//! feature, one pinned dependency, so a protocol shift touches one file set and
//! the executor-agnostic contract layer never learns MCP exists.
//!
//! # Layout
//!
//! - [`McpServer`] (in the `server` submodule) owns one server connection over
//! either transport: [`connect`](McpServer::connect) spawns a child process
//! and speaks stdio, [`connect_http`](McpServer::connect_http) reaches a
//! remote server by URL over streamable HTTP. Either way it initializes the
//! MCP session, lists the tools, and shuts the session down cleanly on close
//! or drop. A stdio server is a real child process of this one, and how it is
//! held (its own process group, kill on drop, and a parent-death signal where
//! the platform has one) is stated in that submodule's docs, along with the
//! one case that can still outlive an operator's `kill -9`.
//! - [`McpTool`] (in the `tool` submodule) is one MCP tool, implementing
//! [`DynTool`](crate::DynTool) directly. Its name, description, and JSON
//! schema are the server's own; its [`Effect`] is decided by the mapping
//! below.
//! - The `park` submodule decodes the park request a server may put under
//! `_meta.salvor` on its result, and owns every refusal a malformed one
//! gets. See the section below.
//! - [`EffectOverrides`] and [`effect_for`] decide a tool's [`Effect`] from the
//! server's annotation hints, subject to per-tool operator overrides.
//! - [`IdempotencyKeys`] carries the operator's per-tool declaration of which
//! input field identifies a call, which is what lets a server's tool
//! participate in cross-run deduplication. See
//! [`IdempotencyPath`](crate::IdempotencyPath) for the key format and the
//! refusal rule.
//!
//! # Effect mapping: hints are not guarantees
//!
//! The MCP specification is explicit that a tool's annotations are *hints* and
//! that a server may lie about them; a client must not make trust decisions on
//! annotations from an untrusted server. So the mapping is conservative:
//!
//! - `readOnlyHint == true` maps to [`Effect::Read`].
//! - otherwise `idempotentHint == true` maps to [`Effect::Idempotent`].
//! - otherwise [`Effect::Write`], the safe default: an unknown tool is presumed
//! to have side effects, because presuming otherwise is the dangerous guess.
//!
//! Because the mapping is only as trustworthy as the server, [`EffectOverrides`]
//! lets the operator pin an [`Effect`] per tool name at connection time. An
//! override wins over whatever the server annotated. That is the operator's
//! trust decision to make: they are asserting "I know this server's `delete`
//! tool is a write regardless of what it claims," and the runtime honors it.
//!
//! # Parking the run, through `_meta`
//!
//! MCP has no field for "park the run that called me," and it does not need
//! one: it has `_meta`, the extension point the specification reserves on
//! every result for metadata a particular client understands. A server that
//! wants to park puts the request under `_meta.salvor`, and a host that is not
//! salvor sees an ordinary tool result carrying an unfamiliar metadata key,
//! which is exactly what `_meta` is for.
//!
//! An [`McpTool`] decodes that after the call returns and yields
//! [`ToolOutcome::Suspend`](crate::ToolOutcome::Suspend) or
//! [`ToolOutcome::Sleep`](crate::ToolOutcome::Sleep), the same values a native
//! tool returns. Nothing above this module distinguishes them, which is the
//! property that keeps the runtime out of it: the completion is recorded
//! first and the park after, claims settle before the wait, and a replayed
//! park never reaches the server again.
//!
//! ```json
//! {"_meta": {"salvor": {"suspend": {
//! "reason": "the claimant must confirm the payout account",
//! "input_schema": {"type": "object", "properties": {"paid": {"type": "boolean"}}},
//! "kind": "signal"
//! }}}}
//! ```
//!
//! ```json
//! {"_meta": {"salvor": {"sleep_until": "2026-08-14T09:00:00Z"}}}
//! ```
//!
//! `kind` is optional and its only value is `"signal"`, an external system
//! owing the run a payload; absent, the run waits on a person. `sleep_until`
//! is an RFC 3339 instant, never a duration, because the runtime records the
//! instant and replay has to reproduce it.
//!
//! A request that is malformed, contradictory (both keys, or either key
//! alongside `isError: true`), or spelled with a key this client does not
//! know is a **tool failure** naming `_meta.salvor` and the problem. It is
//! never quietly passed through as output: a server author who asked for a
//! park and silently got a plain result has no way to see what went wrong.
//! A result with no `_meta.salvor` is untouched and records exactly as it
//! always did.
//!
//! Such a failure is
//! [`ToolError::MalformedResult`](crate::ToolError::MalformedResult), and it
//! costs exactly one execution whatever the tool's effect. A `Read` is
//! otherwise re-run on a failure, but the request that could not be read is
//! already in hand, and calling the same server again produces the same
//! misspelling. Retrying would turn one clear failure into three and delay
//! the only thing the author needs, which is the message.
//!
//! # Client-side input validation is structural only
//!
//! A native [`TypedTool`](crate::TypedTool) validates the model's JSON against
//! a typed `Input` before running. An MCP tool has no typed `Input` on this
//! side of the wire, only the server's declared JSON Schema. This module does
//! *not* embed a JSON Schema validator, so it does not check arguments against
//! that schema. What it does check, locally and before any network hop, is that
//! the arguments are structurally an MCP argument object (a JSON object, or
//! absent): anything else is [`ToolError::InvalidInput`](crate::ToolError::InvalidInput)
//! with no round trip. Semantic validation against the schema is the server's
//! job, and a server-reported failure comes back as
//! [`ToolError::Handler`](crate::ToolError::Handler). See [`McpTool`] for the
//! exact contract.
use BTreeMap;
use ToolAnnotations;
use Effect;
use crateIdempotencyPath;
pub use ;
pub use McpTool;
/// Per-tool [`Effect`] overrides supplied by the operator at connection time.
///
/// An entry pins the [`Effect`] for one tool name, overriding whatever the
/// server annotated (or failed to annotate). This is the operator's trust
/// decision: MCP annotations are hints a server may misstate, so an operator
/// who knows a tool's true side-effect class states it here and the runtime
/// honors it over the wire hints. A tool with no override falls back to the
/// annotation mapping described on the [module docs](crate::mcp).
/// Per-tool idempotency key declarations supplied by the operator at connection
/// time.
///
/// An entry names, for one tool, the input field whose value identifies the
/// operation the call performs. A `pay_claim` entry of `"claim_id"` says that a
/// call carrying `{"claim_id": "wreck-9931"}` *is* the payout for that claim,
/// whichever run asks for it, which is the statement the store needs before it
/// can let exactly one run execute it (see
/// [`RunCtx::tool_call`](../../salvor_runtime/struct.RunCtx.html#method.tool_call)).
///
/// The declaration is the operator's, not the server's, for the same reason
/// [`EffectOverrides`] is: a server's own account of its tools is a hint, and
/// nothing on the wire says which field of a call names the money.
///
/// A tool with no entry is untouched: it declares no key, exactly as before.
/// Decides a tool's [`Effect`] from its name, its server-reported annotations,
/// and the operator's overrides.
///
/// The rule, in precedence order:
///
/// 1. An [`EffectOverrides`] entry for this name wins outright.
/// 2. Else `readOnlyHint == true` gives [`Effect::Read`].
/// 3. Else `idempotentHint == true` gives [`Effect::Idempotent`].
/// 4. Else [`Effect::Write`], the safe default for an unannotated or otherwise
/// unclassified tool.
///
/// Only `Some(true)` counts for a hint; a missing hint or an explicit
/// `Some(false)` is not treated as a promise of read-only or idempotent
/// behavior, which keeps the fall-through on the safe side.