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
//! 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.
//! - [`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.
//! - [`EffectOverrides`] and [`effect_for`] decide a tool's [`Effect`] from the
//! server's annotation hints, subject to per-tool operator overrides.
//!
//! # 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.
//!
//! # Suspension does not apply
//!
//! MCP has no concept of suspending a call to await a human. An [`McpTool`]
//! therefore never returns [`ToolOutcome::Suspend`](crate::ToolOutcome::Suspend);
//! its [`call_json`](crate::DynTool::call_json) resolves to an output or an
//! error, never a parked run. Human-in-the-loop lives on native tools.
//!
//! # 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;
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).
/// 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.