
Mcp Confirm
io.github.les-kv0.2.1更新於 Oct 2, 2026
An MCP server whose confirmation cannot be replayed against a different call.
概覽
一個本機 MCP 伺服器,為刪除檔案的呼叫加上一次性確認、提示文字淨化與執行前複查。
- 功能
- 它包裝了一個只刪除單一檔案的示範工具,補上 MCP SDK 未涵蓋的三項防護:以帳本確保一次核准只能使用一次、淨化顯示給使用者的確認文字、在刪除前複查目標路徑。它會拒絕啟動時指定根目錄以外的路徑,也會拒絕已變成符號連結的路徑。README 說明 SDK 已負責綁定與過期核准狀態,本套件只處理剩下的缺口。
- 適用情境
- 當助理執行刪除檔案或一次性兌換等後果嚴重、難以復原的操作,且同一核准不能被重複使用、確認對話框不能被不可信檔名偽造時,值得考慮。一般唯讀工具不需要它。
- 執行需求
- 以 PyPI 套件 mcp-confirm 透過 stdio 在本機執行,使用 pip 安裝,因此需要 Python 執行環境。啟動時需提供根目錄參數;沒有根目錄時它會拒絕所有請求。未宣告任何帳號、API 金鑰或環境變數。
安裝
在 SourceWeft 中
- 開啟 儀表板中的 Mcp Confirm,將其新增到工作區。
- 為需要使用其工具的對話啟用該服務。
Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。
其他 MCP 客戶端
參照 儲存庫 中的啟動說明。
README
mcp-confirm
In plain terms: when an AI asks "are you sure?", the MCP SDK already stops that approval being reused for a different action. It does not stop the same approval being used twice. This closes that gap, and two others the SDK leaves open.
Read this first: most of this problem is already solved
The 2026-07-28 MCP specification added Multi Round-Trip Requests, so a tool can
pause mid-call and ask the user to confirm. The approval travels back through
the client as an opaque requestState, which the spec says servers MUST
treat as attacker-controlled.
The Python SDK does this for you, by default, on every MCPServer.
RequestStateBoundary is appended to the middleware chain unconditionally —
with an ephemeral key if you supply none. It seals the state under
AES-256-GCM and binds it to the method, the target, a digest of the call's
arguments, the audience, and the authenticated principal, with a TTL.
So the attack everyone reaches for first — approve deleting cache.txt, then
replay that approval against thesis.txt — is already refused by the SDK.
You do not need a library for it, and you should not write one.
This repository originally wrote one anyway: 250 lines of HMAC, TTL and argument binding, shipped as the headline feature, redundant the day it was published. That is recorded at the bottom rather than quietly deleted.
What the SDK does not do
Three gaps, each with a test that fires.
1. It binds and expires the state. It never spends it.
Inside the TTL, the same approval verifies as many times as it is presented. Every check the boundary makes passes, every time. The specification is explicit that this is deliberate and that the rest is yours:
Note that these measures bound the replay window and prevent cross-user and cross-request reuse, but do not by themselves guarantee single-use. Servers for which a given
requestStatemust be consumed at most once (e.g., one-time redemptions) MUST enforce that invariant server-side.
For "delete a file" the second attempt finds nothing. For "transfer £500" it
is the entire problem. singleuse.py is that
invariant — a ledger of outstanding confirmations, spent on redemption. It
contains no cryptography, because the boundary already guarantees the plaintext
is something this server minted.
2. Nothing sanitises the question the human reads
An elicitation message is server-chosen text rendered to a person, usually
with an untrusted value interpolated into it — the whole point is to say which
file is going away. So name a file:
The dialog now carries a second, official-looking prompt. The user is not confirming a deletion; they are being phished by their own tooling. The same 2026-07-28 release also shipped MCP Apps — server-rendered UI — which widens this surface rather than narrowing it.
prompt.py flattens untrusted values to one line,
strips bidirectional overrides and zero-width characters, and truncates from the
middle so the filename at the end stays visible. Control characters become
spaces rather than being deleted, since collapsing them would let a\nb and
ab render identically — two different files, one dialog.
3. No protocol layer can re-check your resource at execution time
The user thought about it in between. The file can be replaced in that window, and only the tool knows what "unchanged" means for it. This server re-checks before deleting, and refuses a path that has become a symlink.
Which layer refuses what
This is the useful part, and the tests are written to demonstrate it. MCPError
means the SDK's middleware refused before this package ran; ToolError means
this package did.
Tests
35 tests — 17 through the server, 11 on the ledger, 7 on prompt sanitisation.
Every server test runs through the real RequestStateBoundary, the same
class MCPServer installs on itself, with a pinned key — sealing round one and
unsealing round two exactly as the wire would.
That harness exists because of a specific mistake. The first version tested by
calling MCPServer.call_tool() directly, which goes straight to the tool
manager and bypasses the middleware chain entirely. The SDK's boundary never
ran, so a redundant hand-rolled guard looked load-bearing. The test design is
what hid it, for an entire build cycle.
Coverage is 81%, with prompt.py at 100% and singleuse.py at 94%. The gap is
main()'s argparse and transport wiring, exercised through build_server
instead.
CI runs on Ubuntu only, deliberately: the swap-after-confirmation tests need symlinks, and the build fails if they report as skipped there.
Install
Run
With no --root, the server refuses every request rather than defaulting to
anything. Roots are fixed at startup and never chosen by the agent. No signing
key is needed — MCPServer brings its own.
Known limitations
- The ledger is in-memory, so it is correct for one process and wrong behind
a load balancer. The SDK supports sharing keys across replicas
(
RequestStateSecurity(keys=[...])); under that configuration replica B rejects a confirmation issued by replica A. It fails closed — the safe direction — but reads to a user as a confirmation that inexplicably stopped working. A multi-process deployment needs Redis or a database row with an atomic compare-and-delete. There is a test for this behaviour. - The demonstration tool is deliberately small. It deletes one file. The
interesting code is
singleuse.pyandprompt.py. - No CVE backs this. MRTR is weeks old, so this is built from the specification's own MUST/SHOULD list and from reading the SDK, not from a published incident.
What was wrong with version 0.1.0
Kept here because a repository that records only its successes is not evidence of anything.
0.1.0 reimplemented what the SDK already did. state.py was 250 lines of
HMAC signing, TTL, and principal/method/argument binding — all duplicating
RequestStateBoundary, none of it as good: HMAC where the SDK uses
authenticated encryption, no audience binding, no key rotation.
It was caught by reading the SDK's source, not by a test. The tests passed precisely because they bypassed the middleware that would have exposed it.
CI separately caught a real bug in 0.1.0: the symlink check ran after
Path.resolve(), so it inspected the link's destination rather than the link
itself. A swap aimed at another file inside an allowed root would have been
deleted. Fixed, with a test for the variant the original never exercised.
0.2.0 deletes state.py entirely and keeps only what the SDK leaves uncovered.
Licence
MIT.
Author
Built by Leslie Kadenge. I do independent security reviews of MCP servers; my public survey of thirteen production servers is at les-k.github.io/field-notes.html.
來源:README.md,提交 a8ae25c
工具
0版本歷史
1- v0.2.1最新Oct 2, 2026

