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
//! The pmcp tool adapter forjar serves over stdio (paiml/forjar#375).
//!
//! # Why forjar owns this and pforge does not
//!
//! `forjar mcp --schema` published `annotations.readOnlyHint` for every tool.
//! The running server published nothing. Measured on 1.24.0 over real stdio,
//! the key set of every one of the twelve tool objects was exactly
//! `['description', 'inputSchema', 'name']` — no `annotations`.
//!
//! The discard point is three layers down and none of them is forjar's:
//! `pforge_config::ToolDef::Native` carries no annotations field, and
//! `pforge_runtime`'s own `PforgeToolAdapter::metadata()` returns a bare
//! `pmcp::types::ToolInfo::new(..)`, which hard-sets `output_schema: None,
//! annotations: None`. Both are byte-identical in pforge 0.2.1, so a version
//! bump fixes nothing. The wire type is willing — pmcp caches `metadata()`
//! verbatim at build time and serialises it whole — so the fix is to hand pmcp
//! a `ToolInfo` forjar filled in.
//!
//! That matters more than a missing field. `Effects::ReadOnly` means "safe for
//! an agent to call unattended", and the argument for it is that an agent
//! consults the hint before deciding it needs a human. It could not: the field
//! was never sent. Six test files asserted `readOnlyHint` and every one of them
//! read `export_schema()` or `mcp --schema`; the single suite that drove real
//! `tools/list` asserted only `name` and `inputSchema`. Agreement between two
//! non-wire surfaces cannot falsify what goes over the wire.
//!
//! # Why `outputSchema` is NOT published, though the same line dropped it
//!
//! It looks free and it is not. MCP 2025-06-18 — the revision this server
//! negotiates, measured — makes an output schema a PROMISE: *"If an output
//! schema is provided: Servers MUST provide structured results that conform to
//! this output schema."* pmcp 1.20 cannot keep it. Its `handle_call_tool` builds
//! `CallToolResult::new(vec![Content::Text { .. }])` and attaches
//! `structuredContent` only through `with_widget_enrichment`, which fires solely
//! for tools carrying ChatGPT widget `_meta`. Nothing a `ToolHandler` returns
//! reaches `structuredContent`, `TypedToolWithOutput` included.
//!
//! So a published `outputSchema` here is an unkeepable promise, and clients
//! enforce it. Driven against this binary with the OFFICIAL MCP TypeScript SDK
//! client (`@modelcontextprotocol/sdk` 1.30.0), which caches each tool's output
//! schema at `listTools()` and checks it on every call:
//!
//! ```text
//! with `info.output_schema = Some(..)`:
//! McpError -32600: Tool forjar_validate has an output schema but did not
//! return structured content
//! without it:
//! content: [{ type: "text", text: "{"valid":true,…}" }], isError: false
//! ```
//!
//! Every `tools/call` failed, on all twelve tools, for the most widely deployed
//! client stack there is — a strictly worse outcome than the missing hint #375
//! was opened for, which at least failed SAFE. `readOnlyHint` carries no such
//! obligation, which is why it is published and this is not.
//! `falsification_mcp_publishes_readonly_hint_over_stdio` asserts the pairing
//! rather than the absence, so the day pmcp can fill `structuredContent` the
//! schema goes back in and the test says so.
//!
//! # What is deliberately NOT changed
//!
//! DISPATCH stays with `pforge_runtime::HandlerRegistry` — the same registry
//! `build_registry()` returns and the tests exercise, so the tested set and the
//! served set still cannot diverge, and `dispatch(&str, &[u8]) -> Vec<u8>`
//! carries no pmcp type across the boundary.
//!
//! The verb table already has a JSON-in/JSON-out entry point, `VerbSpec::invoke`,
//! and wiring it in here is the obvious simplification. DO NOT. `invoke` builds
//! a `tokio::runtime::Runtime` internally (`verb/registry.rs`), and [`handle`] is
//! already running on one, so every `tools/call` would panic with "Cannot start
//! a runtime from within a runtime". Its existing callers — `verb::cli` and
//! `verb::http` — are synchronous, which is why they are fine and this is not.
//! `falsification_mcp_publishes_readonly_hint_over_stdio` dispatches every
//! advertised tool for exactly this reason: an adapter that made that swap would
//! satisfy every annotation assertion and serve a dead surface.
//!
//! [`handle`]: VerbToolAdapter::handle
use Arc;
use HandlerRegistry;
use ;
use Value;
use RwLock;
/// One forjar verb, published to pmcp with the metadata pforge dropped.