Shared Equipment Rescue
Shared Equipment Rescue / 共享儀器搶救
Both versions are kept in this file so nothing goes stale in two places: the English section is the reference, the Chinese section is a faithful translation of the same content (same commands, same numbers, same images).
Shared Equipment Rescue
A facility manager's AI copilot for shared-instrument outage recovery — a self-hosted MCP 2025-11-25 server that detects every booking an outage breaks, proposes a rule-checked replan, and never writes a single change without explicit human confirmation (MCP elicitation, human-in-the-loop).
Amazon "Build, Ship, Shape" hackathon entry — Alexa+ primary track + AWS Builder challenge (Amazon Nova Lite via the Bedrock Converse API). A board refresh uses the scripted sentence. On the simulated page, a bounded tool loop can propose MCP calls. The engine decides every move, and the Confirm click stays human.
The page served from web/ is a simulated Alexa+ experience: a browser MCP client, not a physical Alexa device.
Try it live
Hosted demo: https://shared-equipment-rescue.onrender.com. Nothing to install.
Voice: in desktop Chrome or Edge, press Mic, allow the microphone, and say "Alexa, Confocal A is down until 4pm, please sort out the bookings". No microphone? Open https://shared-equipment-rescue.onrender.com/?utterance=Alexa%2C%20Confocal%20A%20is%20down%20until%204pm%2C%20please%20sort%20out%20the%20bookings, which sends the same line through the page's simulation hook.
The page reports the outage (
v2) and proposes plan P1. Press Apply P1: the Confirm card is a real MCP elicitation. Press Confirm and the board becomesv3.Reset demo data puts the board back to
v1.
Notes: every visitor shares one demo state, so press Reset demo data if someone else has already applied the plan. Bedrock is off on the hosted demo; the chat is labelled Fallback: pattern parser (Bedrock off). Speech recognition is the browser's own Web Speech API: the page calls no cloud speech service itself (Chrome may process the audio on Google's servers). All bookings, people, and instruments are synthetic, and the demo tokens are fake.
Related MCP server: ReadyBy
Testing instructions for judges
Clean-clone setup, then the MCP server (spec 2025-11-25, Streamable HTTP) and the web client. Open http://127.0.0.1:8080. That page is the simulated Alexa+ experience. It sends initialize, tools/list, and tools/call over Streamable HTTP. It is not connected to the Alexa+ developer console.
Clean clone
git clone https://github.com/Fu93/shared-equipment-rescue.git
cd shared-equipment-rescue
python3 -m venv .venv
source .venv/bin/activate
# Windows Command Prompt, instead of the python3 and source lines above:
# python -m venv .venv
# .venv\Scripts\activate
pip install -r requirements.txt
pip install -r requirements-dev.txtOn Windows Command Prompt, use python -m venv .venv and .venv\Scripts\activate instead of the python3 and source .venv/bin/activate lines. Each later terminal needs that same activate step before the server commands.
requirements.txt is the runtime install, including boto3. requirements-dev.txt adds pytest, pytest-asyncio, jsonschema, and playwright. The demo tokens are fake.
On Windows, start Python with python -X utf8 so pytest output stays UTF-8. That flag is optional on Linux and macOS.
Start the MCP server and the web client
Use two terminals, from the repository root, with the virtualenv active. Do not run python -m mcp_server in a third terminal at the same time: both servers bind port 8765.
python tools/dev_server2.pyThat process is the MCP server: spec 2025-11-25, Streamable HTTP, at http://127.0.0.1:8765/mcp, with a fresh temporary database on each boot. python -m mcp_server is the same server pointed at a persistent server.db.
cd web && python app.py --port 8080Open http://127.0.0.1:8080. The right-hand MCP protocol console should show a connection and protocol 2025-11-25. The default identity is Facility Manager (tok-manager).
Golden path
Report an outage. In the chat box send
Confocal A is down until 4pm, or press the button Report outage (Confocal A 10:00–16:00). The version badge becomesv2, an outage banner appears, and bookings B2 and B4 are markedblocked. The page then proposes a replan on its own.Review the proposed replan. The notebook lists two moves: Chen to Confocal B 11:15–12:15, and Wang to Confocal A 16:00–17:00, each with a machine-readable reason. The board shows pending plan P1.
Cancel, then Confirm. Press Apply P1. The confirmation card is a real MCP
elicitation/create. Only a click answers it. Press Cancel. The badge staysv2and no booking moves. Press Apply P1 again, then Confirm. The badge becomesv3and the moves are on the board.Researcher view. Under Acting as, choose Wang (
tok-wang). Wang's own 16:00 slot stays visible. Other bookings on Confocal B showOccupied.Audit log. Open Audit log. The chain badge should show an intact chain. Under
tok-wangthe trail is the researcher view (that user's actions plus public events). Switch back totok-managerto see every row.
Run the tests
python -m pytest tests/ -qMeasured on Linux after pip install -r requirements-dev.txt. On CPython 3.11.17, 3.12.8, and 3.13.5, with the Playwright browser installed (python -m playwright install chromium): 129 passed, 1 skipped. Without that browser: 121 passed, 9 skipped. The commit before the hosted-demo change, on CPython 3.11.17, 3.12.3, and 3.13.16, was 111 passed with the browser and 103 passed, 9 skipped, without it. Without a browser the skips are test_video_walk.py, test_screenshots_current.py, the browser tests in test_voice_hook.py, and test_bedrock_converse_live (AWS_BEARER_TOKEN_BEDROCK was unset). With the browser, only test_bedrock_converse_live skipped. The rate-limit commit before this stub and queue fix, on the same runs, was 108 passed with the browser and 102 passed, 7 skipped, without it. The verification commit before that rate-limit fix, on the same runs, was 99 passed with the browser and 96 passed, 4 skipped, without it. The tool-loop commit before those fixes, on the same runs, was 90 passed with the browser and 87 passed, 4 skipped, without it. The narration-only commit before this tool loop, on the same Linux 3.11 and 3.13 runs, was 73 passed with the browser and 71 passed, 3 skipped, without it. The commit before those Bedrock tests was 70 passed with the browser and 68 passed, 2 skipped, without it.
What is real, and what is simulated
Real in this repository: the MCP server (mcp==1.30.0, spec 2025-11-25, Streamable HTTP); this web page as an MCP client, including server-initiated elicitation; the deterministic replan engine; SQLite, monotonic schedule_version, idempotent plan_id, and the hash-chained audit log. The demo tokens (tok-manager, tok-chen, tok-wang, tok-li) are intentional fakes.
Simulated: the web UI is a simulated Alexa+ experience, labelled Alexa+ (simulated) — MCP client. The bookings, people, and two confocal microscopes are synthetic data. The seeded day is SER_DEMO_DATE, or today in SER_DEMO_TZ (default Europe/London). The published scenario in the tests is 2026-10-20. With Bedrock off, the chat matches a few fixed phrases and labels that path Fallback: pattern parser (Bedrock off). With Bedrock on, the same chat can propose tool calls, bounded at 4 steps. A board refresh does not call Bedrock. Bedrock rephrases the schedule_get overview only when the request sends x-ser-narrate. The model does not decide bookings. The Confirm card stays a click.
What it does
When a confocal microscope (or any shared instrument) goes down mid-day, a manager normally rebooks six papers by hand. This server does the whole recovery loop over MCP:
Report the outage → engine instantly marks affected bookings
blocked, bumpsschedule_version, writes an audit log row.Propose a replan → greedy scheduler searches 15-minute increments, FIFO by deadline, enforcing operating hours, per-user equipment permissions, personal calendars, hard deadlines, and a 15-minute post-session buffer — changes carry machine-readable reason codes; impossible items return per-equipment
earliest_possiblediagnostics (available_only_after,available_after_deadline+deadline_would_need at_least "12:15",no_permissionblockers), never a bare failure.Apply → the tool calls
ctx.elicit()and refuses to mutate until a human confirms in the client UI. Decline / cancel / unsupported client →not_confirmed, zero side effects, plan staysproposed.Idempotency everywhere:
plan_id=(outage_id, based_on_version), monotonicschedule_version, stale proposals rejected (plan_stale), re-apply →already_applied.
The demo, frame by frame
The six frames below are the actual demo (regenerated from this build by
tools/shoot_screenshots.py; tests/test_screenshots_current.py fails if
they ever drift from what the code renders).
1 — the board, straight after initialize — six bookings across two
confocals, and the right-hand panel showing the real MCP traffic
(tools/call → ok, protocol 2025-11-25, identity taken from the header):

2 — one tool call, the outage is recorded — the version badge jumps to
v2, the outage banner appears and B2/B4 are marked blocked:

3 — the proposal — Chen moves to Confocal B 11:15–12:15, Wang to
Confocal A 16:00–17:00, each with a machine-readable reason
(switched equipment (CONFOCAL A → CONFOCAL B), still meets deadline):

4 — the human-in-the-loop gate — the server sends a real MCP
elicitation/create; the confirmation card is answered by a click and
nothing else. No spinner on top of the decision:

5 — applied, v3 — the audit row records who confirmed and when
(London time):

6 — the same board as a researcher — Wang sees his own new 16:00 slot
while everyone else's booking is masked as Occupied:

Quick start
pip install -r requirements.txt # mcp==1.30.0, uvicorn[standard], pydantic>=2.5
python -m mcp_server # → http://127.0.0.1:8765/mcpOptional flags: python -m mcp_server --host 127.0.0.1 --port 8765 --db server.db
Talk to it with any MCP client
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
headers = {"authorization": "Bearer tok-manager"} # header-only auth
async def confirm_outage(message, schema): # MUST be async
# message lists what will move; schema is {"confirm": bool}
return ElicitResult(action="accept", content={"confirm": True})
async with streamablehttp_client("http://127.0.0.1:8765/mcp", headers=headers) as (r, w, _):
async with ClientSession(r, w, elicitation_callback=confirm_outage) as s:
await s.initialize()
res = await s.call_tool("schedule_get", {}) # res.structuredContentClients without an elicitation callback are safe by design: apply returns not_confirmed and nothing changes.
Demo tokens (Authorization: Bearer …)
token | role | can see / do |
| manager | both confocals; write tools |
| researcher | confocal-a + confocal-b |
| researcher | confocal-a only |
| researcher | confocal-b only |
Identity comes only from the per-request header — never from tool arguments.
The eight tools (contracts/)
contracts/tools.json holds the machine-readable input/output schemas (generated verbatim from the live tool registry, locked by tests). contracts/examples/ holds 13 frozen end-to-end fixtures replayed byte-exact by the test-suite deep-equality lock — any drift in a fixture or a schema breaks the build:
tool | writes? | notes |
| no | full day view, role-masked, narration in |
| yes | manager-only; duplicates rejected |
| no (creates plan row) | stale-check + idempotent plan_id |
| yes | elicitation-gated; human confirm required |
| no | who did what, when — managers see the full trail; researchers see own actions + public events (outages, replans, applies, resets). Token hints show only a truncated non-identifying fragment ( |
| no | manager-only; recomputes the SHA-256 hash chain over the audit trail and flags any edit, deletion or splice ( |
| yes | manager-only; restores seed schedule (version 1); clears the audit trail and records the reset itself as the final entry |
| no | echoes elicitation wiring for client bring-up |
Engine guarantees (§10 rules)
Only
movablebookings affected by the outage are rescheduled.FIFO by deadline; ties by original start.
15-min slot grid; original equipment preferred if still available ≥ required duration.
Hard constraints: operating hours, per-user equipment grants, personal calendar (no double-booking), hard deadline, 15-min post-session cleaning buffer.
Greedy, determinstic — no randomness, no LLM decisions in the write path.
Demo day (2026-10-20, Europe/London): outage on Confocal A 10:00–16:00 ⇒ B2 → Confocal B 11:15–12:15 (first slot after Li's immovable B3), B4 → Confocal A 16:00–17:00 (after outage). Failing both (deadline 11:30 scenario) yields per-equipment diagnostics: B could host 11:15–12:15 available_after_deadline + deadline_would_need at_least 12:15; A shows no_slot_today — while B4 still relocates to A 16:00 in the same plan.
LLM layer (dual-mode, §7 of the plan)
mcp_server/llm.py can phrase the schedule_get sentence, and agent_converse proposes tool calls for the chat loop. Neither path chooses slots or moves bookings. schedule_apply_replan still waits for the human Confirm card. No AWS credentials? The demo still runs on the scripted sentence and the pattern parser.
A board refresh calls schedule_get and returns the scripted sentence. That refresh does not call Bedrock. Bedrock rephrases the sentence only when the MCP request includes the header x-ser-narrate set to 1, and only after the engine has already built the bookings, the outage, and any pending plan. The tool loop is a separate call, once per user turn.
When narration is requested:
SER_LLM_MODE=scriptednever calls Bedrock, even if a token is set. The test suite sets this.SER_LLM_MODE=auto(the default) andSER_LLM_MODE=bedrockcall Bedrock only when credentials exist: a non-emptyAWS_BEARER_TOKEN_BEDROCK, orAWS_ACCESS_KEY_IDtogether withAWS_SECRET_ACCESS_KEY, orAWS_PROFILEwith an existing shared credentials or config file, orAWS_CONTAINER_CREDENTIALS_RELATIVE_URI. With none of those, the mode stays scripted and no client is built.The client is one cached
boto3bedrock-runtimeclient, built at startup.BEDROCK_MODEL_IDdefaults to amazon.nova-lite-v1:0.AWS_REGIONdefaults tous-east-1. boto3 readsAWS_BEARER_TOKEN_BEDROCKfrom the environment. The token is not a client argument. When that bearer token is set, the process setsAWS_EC2_METADATA_DISABLED. Converse runs on a worker thread with a timeout. The audited latency is the Converse call itself. The client allows 2 attempts in total viatotal_max_attempts. botocore'smax_attemptscounts only retries after the first call, so a value of 2 there would be 3 attempts.If narration fails or returns no text,
schedule_getreturns the scripted sentence. A failed user turn shows a short error and falls back to the pattern parser for that turn. The visible label is Fallback: pattern parser (Bedrock error: ). The audit row stores the code and the request id, not the raw exception. When Bedrock is not attempted, the label isBedrock: off.Each attempt appends one
bedrock_converseaudit row with the timestamp, model, region, request id, and latency. The token is not in that row. The system prompt forbids inventing facts, overriding the engine, reordering tool arguments, claiming authority, revealing masked data, and posing as the system of record.SER_BEDROCK_STUBset to voice is test-only and never calls AWS. The badge reads Bedrock: STUB (test). The audit row records provider stub and model stub. The outage window usesSER_DEMO_DATEandSER_DEMO_TZ.SER_BEDROCK_STUB_DELAYis an optional test-only pause in seconds.
When the dev panel says Bedrock is on, a typed line, or a final spoken line that starts with the wake word Alexa, goes into a tool loop capped at 4 converse rounds and 4 tool executions. The cap is enforced on the server for that conversation_id. The page stops at 4 as well. Converse receives a toolConfig of the read and propose tools, plus schedule_apply_replan. reset_demo_data, debug_confirm, and verify_audit_chain are not in that list, and the server refuses them if the model asks. The browser runs each accepted call on its existing MCP session, so the Confirm card still appears for schedule_apply_replan. The board refreshes once before that Confirm card, and once when the turn ends. A malformed call, an unknown tool, or arguments that do not match the schema stops the loop, and that call is not executed. The model does not choose slots, move bookings, invent facts, or confirm an apply. Each proposal is an agent_tool_call audit row. A result the browser reports is stored as client-reported. After a confirmed apply the page reports completed and cites schedule_apply_replan with the plan id. A client status of applied, including a lookalike, is refused and writes no audit row. An apply is recorded only by the server's own apply path. The token is not in that row. New conversation_id values for one token are capped, and idle counters expire. GET /metrics and the agent posts use a separate budget keyed by client IP address, the same address as the MCP flood limit. It is not a browser session. A non-JSON or HTTP 429 reply shows a short message, retries, then uses the pattern parser labelled Fallback: pattern parser (rate limit). While Bedrock is off, the chat uses the pattern parser and the page labels it Fallback: pattern parser (Bedrock off). A failed Bedrock call uses the same parser for that turn and labels it Fallback: pattern parser (Bedrock error: ). If that failure happens after this turn already recorded an outage, the parser calls schedule_propose_replan for that outage and continues from the current state.
The Mic button is push-to-talk. It uses SpeechRecognition or webkitSpeechRecognition in the browser, language en-US, and shows the live transcript. A final utterance that starts with Alexa is sent with no Enter key. The reply is shown in the bubble and spoken with speechSynthesis. If the browser has no speech API, or the microphone is denied, a notice stays visible and typing still works. This page does not call a cloud speech service.
serSimulateUtterance and the ?utterance= query are a simulation hook. They wait until the MCP session exists, the schedule has loaded, the Bedrock status has resolved, and any in-flight call has finished, including a pending /agent/step. Calls on one page run one after another, and the queue waits until that turn's tool loop finishes. Then they inject a transcript through the same function as a recognised utterance, so a test or a screen recording with no microphone can drive the turn. The wake word still applies.
The seeded day is SER_DEMO_DATE (an ISO date). When that variable is unset, the day is today's date in SER_DEMO_TZ, which defaults to Europe/London. Audit timestamps stay on the real UTC clock and are not rewritten to the demo day. Start the MCP server and the web shell with the same values. The protocol console says MCP spec 2025-11-25.
Amazon Bedrock
export AWS_BEARER_TOKEN_BEDROCK=your-bedrock-api-key
export AWS_REGION=us-east-1
export BEDROCK_MODEL_ID=amazon.nova-lite-v1:0export SER_DEMO_DATE=2026-10-20
export SER_DEMO_TZ=Europe/Londonboto3 (>=1.39, listed in requirements.txt) builds one cached client at startup. Standard AWS credentials can stand in for the bearer token. Start the MCP server from that environment. The dev panel shows Bedrock: on (amazon.nova-lite-v1:0, us-east-1) from /metrics, without a Converse call on each board refresh. A user turn is what calls Converse. test_bedrock_converse_live makes that one call and skips when AWS_BEARER_TOKEN_BEDROCK is unset. This repository does not use any other AWS service.
Architecture
Simulated Alexa+ (web/, an MCP client) This repo
┌────────────────┐ Streamable HTTP ┌─────────────────────────┐
│ schedule cards │ ──────── tools/call + Confirm ─────▶ │ FastMCP server │
│ elicitation UI │ 127.0.0.1:8765 │ engine + SQLite + audit │
└────────────────┘ └─────────────────────────┘Browser demo (web/) — simulated Alexa+ experience
python -X utf8 tools/dev_server2.py # MCP server on http://127.0.0.1:8765/mcp (fresh temp DB each boot)
cd web && python app.py --port 8080 # web shell on http://127.0.0.1:8080 (any --port works)Or run the MCP server against a persistent DB file: python -X utf8 -m mcp_server.
This UI is the simulated Alexa+ experience. web/ is part of this repo (an MCP Streamable-HTTP client UI): FastAPI serves the
static client and same-origin-proxies /mcp to the MCP server, streaming GET /mcp
SSE chunk-by-chunk so server-initiated elicitation/create reaches the browser live.
The apply confirmation card is answered only by a human click — the demo identity is
carried solely in the Authorization: Bearer <token> header, never in tool arguments.
Hardening (OWASP MCP Security Cheat Sheet)
Transport-level header validation runs at both layers and rejects DNS-rebinding browsers / cross-site attackers before any MCP logic executes:
layer | check | behavior |
web proxy ( |
| present-but-unrecognized → JSON-RPC error |
MCP server ( | pure-ASGI | same policy for anyone bypassing the proxy |
Plus a structured audit trail (audit_log SQLite table, schedule_audit_get
tool, "Audit log" tab in the web UI): every tool call records actor, role, tool,
result, detail and a truncated token hint. Writes are best-effort and can never
break a tool response. Production-grade details: WAL journal mode (concurrent
reads never block on the write lock), cursor pagination (before_id +
tool_filter on schedule_audit_get, next_before_id cursor returned), a
10k-row rolling retention enforced after every audit insert, and a SHA-256
hash chain over the trail — verify_audit_chain (manager-only) recomputes the
chain and flags edits, deletions or splices, and the UI shows a live
"✓ chain N rows" / "⚠ tamper @row" badge. Both the MCP guard middleware and the
web proxy apply a per-IP token-bucket rate limit (~4 req/s, burst 16) that
answers floods with HTTP 429 / -32002 instead of letting them through. The
audit tab carries a tool filter dropdown and 10-second auto-refresh; each
worker thread reuses one thread-local SQLite connection; deployments on a
different proxy port / HTTPS tunnel can whitelist their browser origin via
SER_ALLOWED_ORIGINS (comma-separated exact origins, honoured by both layers).
Tier 3 additions: every audit row also stores a structured JSON mirror
(detail_json) of the event — {tool, result, ...structured keys} such as
plan_id, blocked_count or schedule_version — exposed through
schedule_audit_get and rendered as an expandable json chip in the audit tab,
so SIEM-style log scrapers get machine-readable fields without parsing prose
(the mirror is best-effort and excluded from the hash chain, so its structure
can evolve without invalidating tamper evidence). A GET /metrics
observability endpoint exists on both layers (MCP :8765 and the web proxy
:8080, which aggregates both): process uptime, token-bucket state, schedule
version, table row counts and journal mode — JSON, no-store, before any MCP
session resources are consumed. The UI ships a deep link (/#audit /
/?view=audit) that opens the audit tab pre-connect, covered by a
Playwright suite (28 browser checks covering the full outage→propose→confirm→apply loop: identity select, panels, equipment report, dev console, confirm card, decline path, applied v3, responsive layout - mutation-verified: each check was shown to fail when its defect is injected).
python -X utf8 -m pytest tests/ -qMeasured on Linux. CPython 3.11.17, 3.12.8, and 3.13.5 with the Playwright browser installed: 129 passed, 1 skipped. Without that browser: 121 passed, 9 skipped. Before the hosted-demo change the stub and queue fix, on CPython 3.11.17, 3.12.3, and 3.13.16, was 111 passed with the browser and 103 passed, 9 skipped, without it. Before this stub and queue fix the rate-limit commit was 108 passed with the browser and 102 passed, 7 skipped, without it. Before that rate-limit fix the verification commit was 99 passed with the browser and 96 passed, 4 skipped, without it. Before those fixes the tool-loop commit was 90 passed with the browser and 87 passed, 4 skipped, without it. Before that tool loop the same 3.11 and 3.13 runs were 73 passed with the browser and 71 passed, 3 skipped, without it, and before those Bedrock tests 70 passed with the browser and 68 passed, 2 skipped, without it.
test_engine.py(12): slot search, buffer semantics, permissions, the B4-scoping engine fix, outage-visible-to-search regression, and the choice-key tie-break (earliest start wins; the original equipment wins an exact tie).test_engine_edges.py(6): boundary edges — half-open intervals, zero-duration, zero-buffer adjacency, unknown equipment.test_contract.py(17): schema equality againstcontracts/tools.json+ byte-exact replay of 13 frozen examples underSER_FROZEN_NOW=2026-10-20T12:00:00+01:00.test_server.py(8): real uvicorn + real MCP client over Streamable HTTP — token/tool tables, full outage→propose→apply→already_appliedloop, elicitation declined leaves the plan untouched,plan_staleafter version bump, reset, and a crash/restart cycle that proves outage + pending plan survive a server restart (subprocess, same SQLite file).test_llm.py(14): mode selection with and without credentials, prohibition wording, a stubbed Converse success and a stubbed failure (audit row, token not logged), andtest_bedrock_converse_live, which skips unlessAWS_BEARER_TOKEN_BEDROCKis set.test_accountability.py(7): real-client audit-trail checks — researcher public-visibility SQL boundary, concurrent double-apply applies exactly once (TOCTOU), 403 origin/host rejections land in the hash-chained transport ledger, the ledger survivesreset_demo_data, a replayed apply leaves an audit row with the chain verifying green, audit pagination drops no row at any page boundary (limit 1/2/3 walks over 5 seeded rows), anddebug_confirmelicitation outcomes are audited.test_web_proxy.py(6): theweb/layer — loopbackOrigin/Hostaccepted on any port, foreign hosts/origins still 403, a real MCP client relayed through the proxy sees all 8 tools and a workingschedule_get,/metricsaggregates both layers, and a dead upstream returns 502 with a restart hint.test_video_walk.py(1, 29 scene checks): drives the real UI through the demo video script beat by beat — the six bookings and two confocals, the version bump to v2 with the outage banner, both proposed moves with machine-readable reasons, the confirm card naming the human-in-the-loop rule, Cancel leaving the version untouched, Confirm reaching v3, the identity switch masking other people's slots as "Occupied", and the audit tab with zone-labelled timestamps — so the recording cannot show something the narration does not claim. Needs Playwright; skips cleanly where no browser can launch.test_screenshots_current.py(1): the committed screenshots indocs/screenshots/must still show what the build renders today — it re-renders the same scenes and compares the recorded text, so the images in the README cannot silently go stale. Needs Playwright; skips cleanly where no browser can launch.test_hosted_deploy.py(18): the opt-in hosted-demo settings — public Host allowlist with numeric-port check, edge client-IP header (never X-Forwarded-For), proxy overwritesx-ser-client-ip, the loopback-only server key, the platformHEAD /probe ledgered ashost_probewhile real foreign hosts stayhost,deploy/render_start.shstartup timeout and exit reporting, and the light-only page.test_readme_i18n.py(2): the English and Chinese sections of this file must agree — same commands, same images, same identifiers, same numbers. A bilingual README that drifts is worse than none, because a reader cannot tell. The second test checks the live rendered page, so the language switch is proven to jump.
Windows note: use python -X utf8 to keep pytest output clean.
Demo screenshots
python -X utf8 tools/shoot_screenshots.pyRegenerates the six demo screenshots from the current build, one per beat of the video script (initial board, outage, proposal, confirm card, applied, and the identity switch). It boots its own servers on ephemeral ports, so it never disturbs a running demo.
It writes two copies: docs/screenshots/ (version-controlled — what the README
shows and what a judge sees) and web/ (the live capture, gitignored — your
recording scratch). Alongside the images it writes docs/screenshots/manifest.json,
the text each frame displayed; tests/test_screenshots_current.py re-renders
the same scenes and compares the text, so a committed screenshot can never
silently go stale.
Development docs
docs/friction-log.md— 7 hard-won findings (SSE-only elicitation, async-callback requirement,Context.request_context.request.headers, engine visibility fix,SER_FROZEN_NOWclock freeze).docs/product-feedback.md— MCP-ecosystem observations (elicitation gaps, json_response hazard).docs/dev-notes/COLLAB/CONTRACT-FROZEN.md— the partner contract;docs/dev-notes/COLLAB/OUTBOX.mdhas the client wiring guide. Internal notes live indocs/dev-notes/. The ad-hoc probes that used to betools/_deep4.pythroughtools/_deep8.pylive inarchive/.tools/check_readme_i18n.py— keeps this file's two language sections in sync (code blocks, images, identifiers, numbers). Run it after editing either side;tests/test_readme_i18n.pyruns it as part of the suite.
Tech choices
Component | Choice | Why |
MCP SDK |
| official, 2025-11-25 spec, elicitation support |
Transport | Streamable HTTP (SSE responses) | required for server→client elicitation |
DB | SQLite ( | transactional versioning + audit log, zero ops |
Narration | Amazon Nova Lite via Bedrock Converse | Advisory wording of the |
License
MIT — see LICENSE. Synthetic demo data; not production-hardened.
共享儀器搶救
共用儀器故障復原的 AI 助手 —— 一個自架的 MCP 2025-11-25 伺服器:偵測故障會影響哪些預約、提出通過規則檢查的重排方案,而且在人類明確確認之前,絕不寫入任何變更(MCP elicitation,人類在環)。
Amazon「Build, Ship, Shape」黑客松參賽作品 —— Alexa+ 主賽道 + AWS Builder 挑戰(Amazon Nova Lite,經 Bedrock Converse API)。看板重新整理用的是 scripted 句子。在模擬頁面上,有上限的工具迴圈可以提出 MCP 呼叫。每一次搬移都由引擎決定,Confirm 點擊仍是人類的。
web/ 提供的頁面是 simulated Alexa+ experience(模擬的 Alexa+ 體驗):瀏覽器裡的 MCP 客戶端,不是實體 Alexa 裝置。
線上試用
託管的 demo:https://shared-equipment-rescue.onrender.com。不需要安裝任何東西。
**語音:**在桌機版 Chrome 或 Edge 按 Mic,允許使用麥克風,然後說 "Alexa, Confocal A is down until 4pm, please sort out the bookings"。沒有麥克風?開啟 https://shared-equipment-rescue.onrender.com/?utterance=Alexa%2C%20Confocal%20A%20is%20down%20until%204pm%2C%20please%20sort%20out%20the%20bookings,它會經由頁面的模擬 hook 送出同一句話。
頁面會記錄故障(
v2)並提出方案 P1。按 Apply P1:Confirm 卡片是真正的 MCP elicitation。按 Confirm,看板會變成v3。Reset demo data 會把看板恢復成
v1。
注意:所有訪客共用同一份 demo 狀態,如果別人已經套用了方案,請按 Reset demo data。託管的 demo 關閉了 Bedrock;聊天區標示為 Fallback: pattern parser (Bedrock off)。語音辨識用的是瀏覽器自己的 Web Speech API:頁面本身不呼叫任何雲端語音服務(Chrome 可能會在 Google 的伺服器上處理音訊)。所有預約、人員與儀器都是合成資料,demo token 也是假的。
評審測試說明
以下是乾淨 clone 的步驟,然後啟動 MCP 伺服器(spec 2025-11-25,Streamable HTTP)與 web client。開啟 http://127.0.0.1:8080。那個頁面就是 simulated Alexa+ experience。它透過 Streamable HTTP 送出 initialize、tools/list、tools/call。它沒有連到 Alexa+ developer console。
乾淨 clone
git clone https://github.com/Fu93/shared-equipment-rescue.git
cd shared-equipment-rescue
python3 -m venv .venv
source .venv/bin/activate
# Windows 命令提示字元,用來取代上面的 python3 與 source 兩行:
# python -m venv .venv
# .venv\Scripts\activate
pip install -r requirements.txt
pip install -r requirements-dev.txt在 Windows 命令提示字元,請改用 python -m venv .venv 與 .venv\Scripts\activate,不要用上面的 python3 與 source .venv/bin/activate。之後每個新終端機在跑伺服器指令前,都要再做一次同樣的啟動。
requirements.txt 是執行期安裝,含 boto3。requirements-dev.txt 另外安裝 pytest、pytest-asyncio、jsonschema、playwright。示範 token 是假的。
Windows 請用 python -X utf8 啟動,pytest 輸出才會維持 UTF-8。這個旗標在 Linux 與 macOS 可選。
啟動 MCP 伺服器與 web client
從儲存庫根目錄開兩個終端機,virtualenv 要在作用中。不要同時再開第三個終端機跑 python -m mcp_server:兩個伺服器都會綁 port 8765。
python tools/dev_server2.py這個行程就是 MCP 伺服器:spec 2025-11-25、Streamable HTTP、位址 http://127.0.0.1:8765/mcp,每次啟動都用一份新的暫存資料庫。python -m mcp_server 是同一個伺服器,資料庫改為持久的 server.db。
cd web && python app.py --port 8080開啟 http://127.0.0.1:8080。右側 MCP protocol console 應顯示已連線,協定為 2025-11-25。預設身分是 Facility Manager(tok-manager)。
主要示範路徑
回報故障。 在對話框送出
Confocal A is down until 4pm,或按 Report outage (Confocal A 10:00–16:00)。版本徽章變成v2,出現故障橫幅,預約 B2 與 B4 標成blocked。頁面接著會自己提出重排。看提出的重排。 筆記本列出兩筆移動:Chen 到 Confocal B 11:15–12:15,Wang 到 Confocal A 16:00–17:00,每筆都有機器可讀的原因。看板顯示待處理方案 P1。
先 Cancel,再 Confirm。 按 Apply P1。確認卡是真正的 MCP
elicitation/create。只有點擊能回答。按 Cancel。徽章維持v2,沒有預約被搬走。再按一次 Apply P1,然後 Confirm。徽章變成v3,移動出現在看板上。研究員視角。 在 Acting as 選擇 Wang(
tok-wang)。Wang 自己的 16:00 時段仍看得到。Confocal B 上其他人的預約顯示Occupied。審計紀錄。 打開 Audit log。鏈徽章應顯示鏈是完整的。在
tok-wang之下,軌跡是研究員視角(該使用者自己的操作加上公開事件)。切回tok-manager可看到每一列。
執行測試
python -m pytest tests/ -q在 Linux 上,於 pip install -r requirements-dev.txt 之後量測。CPython 3.11.17、3.12.8 與 3.13.5,已安裝 Playwright 瀏覽器(python -m playwright install chromium):129 passed、1 skipped。沒有那個瀏覽器:121 passed、9 skipped。託管 demo 變更之前的 commit,在 CPython 3.11.17、3.12.3 與 3.13.16 上是有瀏覽器 111 passed、沒有瀏覽器 103 passed、9 skipped。沒有瀏覽器時跳過的是 test_video_walk.py、test_screenshots_current.py、test_voice_hook.py 裡的瀏覽器測試,以及 test_bedrock_converse_live(當時 AWS_BEARER_TOKEN_BEDROCK 沒有設定)。有瀏覽器時只跳過 test_bedrock_converse_live。這次 stub 與佇列修正之前的速率限制 commit,在同樣的測試上是有瀏覽器 108 passed、沒有瀏覽器 102 passed、7 skipped。那次速率限制修正之前的驗證 commit,在同樣的測試上是有瀏覽器 99 passed、沒有瀏覽器 96 passed、4 skipped。那些修正之前的工具迴圈 commit,在同樣的測試上是有瀏覽器 90 passed、沒有瀏覽器 87 passed、4 skipped。這個工具迴圈之前、只有敘事的那個 commit,在同樣的 Linux 3.11 與 3.13 上是有瀏覽器 73 passed、沒有瀏覽器 71 passed、3 skipped。那些 Bedrock 測試加入之前的 commit 是有瀏覽器 70 passed、沒有瀏覽器 68 passed、2 skipped。
什麼是真的,什麼是模擬的
這個儲存庫裡真的有: MCP 伺服器(mcp==1.30.0、spec 2025-11-25、Streamable HTTP);這個網頁作為 MCP 客戶端,含伺服器發起的 elicitation;確定性的重排引擎;SQLite、單調的 schedule_version、冪等的 plan_id,以及雜湊鏈審計紀錄。示範 token(tok-manager、tok-chen、tok-wang、tok-li)是故意的假 token。
模擬的: web UI 是 simulated Alexa+ experience,標題是 Alexa+ (simulated) — MCP client。預約、人物,以及兩台共軛焦顯微鏡都是合成資料。種子日是 SER_DEMO_DATE,沒設定時就是 SER_DEMO_TZ 的今天(預設 Europe/London)。測試裡發表的情境是 2026-10-20。Bedrock 關閉時,對話框比對幾句固定說法,並把那條路徑標成 Fallback: pattern parser (Bedrock off)。Bedrock 開啟時,同一個對話框可以提出工具呼叫,上限 4 步。看板重新整理不會呼叫 Bedrock。只有請求帶了 x-ser-narrate 時,Bedrock 才會改寫 schedule_get 的總覽句。模型不決定預約。Confirm 卡仍是一次點擊。
它在做什麼
當共軛焦顯微鏡(或任何共用儀器)白天中途故障,管理員通常得手工改期六篇論文。這個伺服器用 MCP 跑完整條復原迴路:
回報故障 → 引擎立刻把受影響的預約標成
blocked、schedule_version遞增、寫入一筆審計紀錄。提出重排方案 → 貪婪排程器以 15 分鐘為網格搜尋、依截止時間 FIFO,遵守營業時間、每人的設備權限、個人行事曆、硬性截止,以及場次後 15 分鐘緩衝 —— 每次異動都帶機器可讀的原因碼;排不進去的項目會回傳 per-equipment
earliest_possible診斷(available_only_after、available_after_deadline+deadline_would_need at_least "12:15"、no_permission阻擋原因),絕不是一句「做不到」。套用 → 工具呼叫
ctx.elicit(),在客戶端 UI 有人工確認之前拒絕任何寫入。拒絕/取消/不支援 elicitation 的客戶端 →not_confirmed,零副作用,方案維持proposed。處處冪等:
plan_id=(outage_id, based_on_version)、schedule_version單調遞增、過期方案被拒(plan_stale)、重複套用 →already_applied。
示範畫面,逐格看
下面六張就是實際的 demo(由 tools/shoot_screenshots.py 依當前版本重新產生;tests/test_screenshots_current.py 會在它們與程式實際渲染的內容不一致時報錯)。
1 — initialize 之後的看板 —— 兩台共軛焦上六個預約,右側面板是真正的 MCP 流量(tools/call → ok、協定 2025-11-25、身分只來自標頭):

2 — 一次工具呼叫,故障被記錄 —— 版本徽章跳到 v2、出現故障橫幅、B2/B4 標成 blocked:

3 — 重排方案 —— Chen 改到 Confocal B 11:15–12:15、Wang 改到 Confocal A 16:00–17:00,每筆都附機器可讀的原因(switched equipment (CONFOCAL A → CONFOCAL B)、still meets deadline):

4 — 人類在環的閘門 —— 伺服器送出真正的 MCP elicitation/create;確認卡只由點擊回答,別無他法。決策之上沒有任何轉圈遮罩:

5 — 已套用,v3 —— 審計列記錄了是誰確認、何時確認(倫敦時間):

6 — 同一張看板,以研究員身分看 —— Wang 看得到自己新的 16:00 時段,其他人的預約則被遮蔽成 Occupied:

快速開始
pip install -r requirements.txt # mcp==1.30.0, uvicorn[standard], pydantic>=2.5
python -m mcp_server # → http://127.0.0.1:8765/mcp可選參數:python -m mcp_server --host 127.0.0.1 --port 8765 --db server.db
用任何 MCP 客戶端與它對話
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
headers = {"authorization": "Bearer tok-manager"} # 只認標頭
async def confirm_outage(message, schema): # 必須是 async
# message 會列出哪些預約要動;schema 是 {"confirm": bool}
return ElicitResult(action="accept", content={"confirm": True})
async with streamablehttp_client("http://127.0.0.1:8765/mcp", headers=headers) as (r, w, _):
async with ClientSession(r, w, elicitation_callback=confirm_outage) as s:
await s.initialize()
res = await s.call_tool("schedule_get", {}) # res.structuredContent沒有 elicitation callback 的客戶端依設計是安全的:套用會回 not_confirmed,什麼都不會變。
示範 token(Authorization: Bearer …)
token | 角色 | 能看/能做 |
| 管理員 | 兩台共軛焦;可寫入的工具 |
| 研究員 | confocal-a + confocal-b |
| 研究員 | 只有 confocal-a |
| 研究員 | 只有 confocal-b |
身分只來自每個請求的標頭 —— 絕不從工具參數取得。
八個工具(contracts/)
contracts/tools.json 放的是機器可讀的輸入/輸出 schema(逐字由線上的工具註冊表產生,並由測試鎖死)。contracts/examples/ 放的是 13 份凍結的端到端範例,由測試套件的深度相等鎖定逐位元重播 —— 範例或 schema 只要有任何漂移就會讓建置失敗:
工具 | 會寫入? | 說明 |
| 否 | 整日視圖,依角色遮蔽,敘事放在 |
| 是 | 僅限管理員;重複回報會被拒 |
| 否(會建立 plan 列) | 過期檢查 + 冪等的 plan_id |
| 是 | 以 elicitation 為閘門;必須人工確認 |
| 否 | 誰、在什麼時候、做了什麼 —— 管理員看完整軌跡;研究員只看自己的操作 + 公開事件(故障、重排、套用、重置)。token 只顯示截斷且不可識別的片段( |
| 否 | 僅限管理員;重算審計軌跡上的 SHA-256 雜湊鏈,標出任何竄改、刪除或拼接( |
| 是 | 僅限管理員;還原成種子排程(版本 1);清空審計軌跡,並把這次重置本身記為最後一筆 |
| 否 | 讓客戶端接線時確認 elicitation 串接是否正確 |
引擎保證(§10 條規則)
只有受故障影響且可移動(
movable)的預約會被重排。依截止時間 FIFO;同截止者依原始開始時間。
15 分鐘時段網格;若原設備仍能提供所需時長,優先留在原設備。
硬性限制:營業時間、每人的設備授權、個人行事曆(不得重複預約)、硬性截止、場次後 15 分鐘清潔緩衝。
貪婪且確定性 —— 沒有隨機性,寫入路徑上沒有任何 LLM 決策。
示範日(2026-10-20,Europe/London): Confocal A 於 10:00–16:00 故障 ⇒ B2 → Confocal B 11:15–12:15(Li 不可移動的 B3 之後第一個空檔)、B4 → Confocal A 16:00–17:00(故障結束後)。若兩邊都不行(截止 11:30 的情境)會回傳 per-equipment 診斷:B 可承接 11:15–12:15,標為 available_after_deadline + deadline_would_need at_least 12:15;A 顯示 no_slot_today —— 而同一份方案裡 B4 仍然移到 A 的 16:00。
LLM 層(雙模式,計畫 §7)
mcp_server/llm.py 可以改寫 schedule_get 那句話,agent_converse 則為對話迴圈提出工具呼叫。兩條路徑都不選時段、也不搬預約。schedule_apply_replan 仍然要等人類的 Confirm 卡。沒有 AWS 憑證?demo 仍用 scripted 那句話和 pattern parser 跑完。
看板重新整理會呼叫 schedule_get 並回傳 scripted 句子。那次重新整理不會呼叫 Bedrock。只有 MCP 請求帶了標頭 x-ser-narrate 且值為 1 時,Bedrock 才改寫那句話,而且是在引擎已經組好預約、故障與待處理方案之後。工具迴圈是另外一通呼叫,每一輪使用者的話一次。
要求敘事時:
SER_LLM_MODE=scripted即使有 token 也不呼叫 Bedrock。測試套件就是這樣設的。SER_LLM_MODE=auto(預設)與SER_LLM_MODE=bedrock只有在憑證存在時才呼叫:非空的AWS_BEARER_TOKEN_BEDROCK,或AWS_ACCESS_KEY_ID加上AWS_SECRET_ACCESS_KEY,或AWS_PROFILE指向已存在的共用 credentials 或 config 檔,或AWS_CONTAINER_CREDENTIALS_RELATIVE_URI。這些都沒有時,模式維持 scripted,不會建立 client。Client 是啟動時建立的一個快取過的
boto3bedrock-runtimeclient。BEDROCK_MODEL_ID預設 amazon.nova-lite-v1:0。AWS_REGION預設us-east-1。boto3 自己從環境讀AWS_BEARER_TOKEN_BEDROCK。token 不是 client 參數。設定了這個 bearer token 時,行程會設定AWS_EC2_METADATA_DISABLED。Converse 在工作執行緒上跑,並有逾時。審計裡的 latency 只算 Converse 那一次呼叫。Client 透過total_max_attempts總共允許 2 次嘗試。botocore 的max_attempts只計算第一次呼叫之後的重試,所以那裡的 2 會變成 3 次嘗試。敘事失敗或沒有文字時,
schedule_get回傳 scripted 那句。使用者這一輪如果呼叫失敗,會顯示一句短錯誤,並在這一輪改走 pattern parser。畫面上的標籤是 Fallback: pattern parser (Bedrock error: )。審計列存的是 code 和 request id,不是原始例外。沒有嘗試 Bedrock 時,標籤是Bedrock: off。每一次嘗試追加一筆
bedrock_converse審計列,內容是 timestamp、model、region、request id、latency。那一列沒有 token。system prompt 禁止捏造事實、覆寫引擎、重排工具參數、宣稱權限、揭露被遮蔽的資料,以及自稱是唯一真實紀錄。SER_BEDROCK_STUB設為 voice 只供測試,不會呼叫 AWS。徽章顯示 Bedrock: STUB (test)。審計列的 provider 與 model 都是 stub。故障時段使用SER_DEMO_DATE與SER_DEMO_TZ。SER_BEDROCK_STUB_DELAY是可選的測試用暫停,單位是秒。
當 dev panel 顯示 Bedrock 為 on,打字的一行,或一句以喚醒詞 Alexa 開頭的最終語音,會進入工具迴圈。上限是 4 輪 Converse 與 4 次工具執行。這個上限由伺服器按那個 conversation_id 執行。頁面也會在 4 步停下。Converse 收到的 toolConfig 是讀取與提案工具,再加上 schedule_apply_replan。reset_demo_data、debug_confirm 與 verify_audit_chain 不在那份清單裡,模型如果開口要,伺服器會拒絕。瀏覽器在自己既有的 MCP session 上執行每一個被接受的呼叫,所以 schedule_apply_replan 仍會出現 Confirm 卡。看板會在那張 Confirm 卡出現前重新整理一次,這一輪結束時再整理一次。格式錯誤的呼叫、未知的工具,或不符合 schema 的參數會停掉迴圈,那個呼叫不會被執行。模型不選時段、不搬預約、不捏造事實、也不確認套用。每一次提案是一筆 agent_tool_call 審計列。瀏覽器回報的結果存成 client-reported。確認套用之後,頁面回報 completed,並註明 schedule_apply_replan 與 plan id。客戶端狀態若是 applied,或長得像它的字,會被拒絕且不寫審計列。套用只由伺服器自己的 apply 路徑記錄。那一列沒有 token。同一個 token 能開的新 conversation_id 有上限,閒置的計數會過期。GET /metrics 與 agent 的 POST 使用另一個依客戶端 IP 位址計算的預算,和 MCP 洪水限制用同一個位址。它不是瀏覽器 session。非 JSON 或 HTTP 429 的回覆會顯示一句短訊息、重試,然後改用標成 Fallback: pattern parser (rate limit) 的 pattern parser。Bedrock 關閉時,對話框使用 pattern parser,頁面把它標成 Fallback: pattern parser (Bedrock off)。Bedrock 呼叫失敗時,這一輪同樣改用這個 parser,並標成 Fallback: pattern parser (Bedrock error: )。若這次失敗發生在這一輪已經記錄故障之後,parser 會對那個故障呼叫 schedule_propose_replan,並從目前的狀態繼續。
Mic 按鈕是 push-to-talk。它在瀏覽器裡使用 SpeechRecognition 或 webkitSpeechRecognition,語言 en-US,並顯示即時逐字稿。一句以 Alexa 開頭的最終語句不需要按 Enter 就會送出。回覆顯示在氣泡裡,並用 speechSynthesis 唸出來。瀏覽器沒有語音 API,或麥克風被拒絕時,畫面上會留著提示,打字仍然可用。這個頁面不呼叫雲端語音服務。
serSimulateUtterance 與 ?utterance= 查詢是 simulation hook。它們會等到 MCP session 存在、排程已載入、Bedrock 狀態已解析,而且沒有進行中的呼叫,包含尚未回來的 /agent/step。同一個頁面上的呼叫會一個接一個跑,佇列會等到這一輪的工具迴圈結束。然後才把一段逐字稿送進與辨識結果相同的函式,所以沒有麥克風的測試或螢幕錄影也能驅動這一輪。喚醒詞仍然要有。
種子日是 SER_DEMO_DATE(ISO 日期)。這個變數沒設定時,那天是 SER_DEMO_TZ 的今天,預設 Europe/London。審計時間戳維持真實的 UTC 時鐘,不會被改寫成示範日。MCP server 與 web shell 要用同一組值啟動。協定主控台寫的是 MCP spec 2025-11-25。
Amazon Bedrock
export AWS_BEARER_TOKEN_BEDROCK=your-bedrock-api-key
export AWS_REGION=us-east-1
export BEDROCK_MODEL_ID=amazon.nova-lite-v1:0export SER_DEMO_DATE=2026-10-20
export SER_DEMO_TZ=Europe/Londonboto3(requirements.txt 裡的 >=1.39)在啟動時建立一個快取過的 client。標準 AWS 憑證可以取代 bearer token。從那個環境啟動 MCP server。dev panel 從 /metrics 顯示 Bedrock: on (amazon.nova-lite-v1:0, us-east-1),看板每次重新整理不會打一通 Converse。使用者的一輪才會呼叫 Converse。test_bedrock_converse_live 打這一通,AWS_BEARER_TOKEN_BEDROCK 沒設定時跳過。這個儲存庫不使用任何其他 AWS 服務。
架構
Simulated Alexa+ (web/, an MCP client) This repo
┌────────────────┐ Streamable HTTP ┌─────────────────────────┐
│ schedule cards │ ──────── tools/call + Confirm ─────▶ │ FastMCP server │
│ elicitation UI │ 127.0.0.1:8765 │ engine + SQLite + audit │
└────────────────┘ └─────────────────────────┘瀏覽器 demo(web/)—— simulated Alexa+ experience
python -X utf8 tools/dev_server2.py # MCP server on http://127.0.0.1:8765/mcp (fresh temp DB each boot)
cd web && python app.py --port 8080 # web shell on http://127.0.0.1:8080 (any --port works)也可以讓 MCP server 對著持久化的 DB 檔跑:python -X utf8 -m mcp_server。
這個 UI 就是 simulated Alexa+ experience。web/ 是這個 repo 的一部分(本身是一個 MCP Streamable-HTTP 客戶端 UI):FastAPI 提供靜態頁,並以同源代理把 /mcp 轉到 MCP server,GET /mcp 的 SSE 會逐塊轉發,所以伺服器發起的 elicitation/create 能即時抵達瀏覽器。套用確認卡只由人類點擊回答 —— demo 身分只承載在 Authorization: Bearer <token> 標頭裡,絕不放在工具參數中。
強化(OWASP MCP Security Cheat Sheet)
傳輸層的標頭驗證在兩層都跑,在任何 MCP 邏輯執行前就先擋掉 DNS-rebinding 瀏覽器/跨站攻擊者:
層 | 檢查 | 行為 |
web 代理( |
| 有但不認識 → JSON-RPC 錯誤 |
MCP server( | 純 ASGI 的 | 繞過代理直接連也套用同一套政策 |
另外還有結構化的審計軌跡(audit_log SQLite 資料表、schedule_audit_get 工具、web UI 的「Audit log」分頁):每次工具呼叫都記下操作者、角色、工具、結果、明細與截斷的 token 提示。寫入是 best-effort,永遠不會讓工具回應出錯。產品級細節:WAL journal 模式(併發讀取不會卡在寫入鎖上)、游標分頁(schedule_audit_get 支援 before_id + tool_filter,回傳 next_before_id 游標)、一萬筆的滾動保留(每次審計寫入後強制執行),以及覆蓋整條軌跡的 SHA-256 雜湊鏈 —— verify_audit_chain(僅限管理員)會重算整條鏈並標出竄改、刪除或拼接,UI 上還有即時的「✓ chain N rows」/「⚠ tamper @row」徽章。MCP 的 guard middleware 與 web 代理都套用每 IP 的 token-bucket 速率限制(約 4 req/s、burst 16),遇到洪水以 HTTP 429/-32002 回應而不是放行。審計分頁有工具篩選下拉選單與 10 秒自動更新;每個 worker 執行緒重複使用一條執行緒層級的 SQLite 連線;若部署在不同的代理 port/HTTPS tunnel,可用 SER_ALLOWED_ORIGINS(逗號分隔的精確 origin,兩層都遵守)白名單化瀏覽器來源。
Tier 3 加項:每一筆審計紀錄還會存一份事件的結構化 JSON 鏡像(detail_json)—— 例如 {tool, result, ...} 加 plan_id、blocked_count 或 schedule_version —— 透過 schedule_audit_get 公開,並在審計分頁呈現成可展開的 json chip,讓 SIEM 類型的日誌擷取器不必解析散文就能拿到機器可讀欄位(這個鏡像屬 best-effort 且不含在雜湊鏈裡,所以它可以演進而不會破壞防篡改證據)。兩層都有 GET /metrics 觀測端點(MCP 在 :8765,web 代理在 :8080 並聚合兩者):行程運行時間、token-bucket 狀態、排程版本、資料表列數與 journal 模式 —— JSON、no-store,且在消耗任何 MCP session 資源之前就可取得。UI 另有深連結(/#audit / /?view=audit)可直接開啟審計分頁,由一套 Playwright 測試涵蓋(28 項瀏覽器檢查,完整走過故障→提出→確認→套用:身分切換、面板、故障回報、dev console、確認卡、拒絕路徑、套用後 v3、響應式版面 —— 並以 mutation 驗證:每條檢查都實測過在注入對應缺陷時會失敗)。
python -X utf8 -m pytest tests/ -q在 Linux 上量測。CPython 3.11.17、3.12.8 與 3.13.5,已安裝 Playwright 瀏覽器:129 passed、1 skipped。沒有那個瀏覽器:121 passed、9 skipped。託管 demo 變更之前,stub 與佇列修正那次在 CPython 3.11.17、3.12.3 與 3.13.16 上是有瀏覽器 111 passed、沒有瀏覽器 103 passed、9 skipped。這次 stub 與佇列修正之前的速率限制 commit 是有瀏覽器 108 passed、沒有瀏覽器 102 passed、7 skipped。那次速率限制修正之前的驗證 commit 是有瀏覽器 99 passed、沒有瀏覽器 96 passed、4 skipped。那些修正之前的工具迴圈 commit 是有瀏覽器 90 passed、沒有瀏覽器 87 passed、4 skipped。這個工具迴圈之前,同樣的 3.11 與 3.13 是有瀏覽器 73 passed、沒有瀏覽器 71 passed、3 skipped;那些 Bedrock 測試加入之前是有瀏覽器 70 passed、沒有瀏覽器 68 passed、2 skipped。
test_engine.py(12):時段搜尋、緩衝語意、權限、B4 範圍的引擎修正、故障可見於搜尋的回歸,以及選擇鍵的平手規則(最早開始者勝;完全平手時留在原設備)。test_engine_edges.py(6):邊界 —— 半開區間、零時長、零緩衝相鄰、未知設備。test_contract.py(17):對contracts/tools.json的 schema 相等性 + 在SER_FROZEN_NOW=2026-10-20T12:00:00+01:00下逐位元重播 13 份凍結範例。test_server.py(8):走 Streamable HTTP 的真 uvicorn + 真 MCP client —— token/工具表、完整的故障→提出→套用→already_applied迴路、拒絕 elicitation 後方案原封不動、版本前進後的plan_stale、重置,以及一次當機/重啟循環(子行程、同一個 SQLite 檔)證明故障與待套用方案能跨越伺服器重啟存活。test_llm.py(14):有/無憑證的模式選擇、禁令措辭、一次 stub 的 Converse 成功與一次 stub 的失敗(審計列、token 不入紀錄),以及test_bedrock_converse_live(AWS_BEARER_TOKEN_BEDROCK沒設定就跳過)。test_accountability.py(7):真 client 的審計軌跡檢查 —— 研究員可見性的 SQL 邊界、併發重複套用只會套用一次(TOCTOU)、403 origin/host 拒絕會落進雜湊鏈的傳輸帳本、帳本在reset_demo_data後仍存、重播套用會留下鏈驗證為綠的審計列、審計分頁在任何頁邊界都不掉列(limit 1/2/3 走過 5 筆種子列),以及debug_confirm的 elicitation 結果會被審計。test_web_proxy.py(6):web/那一層 —— loopback 的Origin/Host在任何 port 都接受、外部 host/origin 依然 403、經代理轉發的真 MCP client 看得到全部 8 個工具且schedule_get可用、/metrics聚合兩層,以及上游掛掉時回 502 加上重啟提示。test_video_walk.py(1 項測試、29 條場景檢查):逐幕驅動真實 UI 走完 demo 影片腳本 —— 六個預約與兩台共軛焦、版本跳 v2 與故障橫幅、兩筆異動與機器可讀原因、確認卡寫明人類在環規則、取消後版本不變、確認後到 v3、切换身分後他人槽位顯示「Occupied」、審計分頁的時區標註 —— 所以錄影不可能出現旁白沒說的畫面。需要 Playwright;沒有瀏覽器時會乾淨跳過。test_screenshots_current.py(1):docs/screenshots/裡已提交的截圖必須仍反映當前版本 —— 它會重跑同樣的場景並比對記錄下來的文字,所以 README 的圖不可能在無聲無息中過期。需要 Playwright;沒有瀏覽器時會乾淨跳過。test_hosted_deploy.py(18):託管 demo 的選用設定 —— 公開 Host 允許清單與數字 port 檢查、edge 設定的 client IP 標頭(絕不用 X-Forwarded-For)、proxy 覆寫x-ser-client-ip、只信任 loopback 的伺服器 key、平台的HEAD /探測記為host_probe而真正的外來 Host 仍記為host、deploy/render_start.sh的啟動逾時與結束回報,以及只用淺色的頁面。test_readme_i18n.py(2):本檔的英文與中文兩節必須一致 —— 同樣的指令、同樣的圖片、同樣的識別字、同樣的數字。會漂移的雙語 README 比沒有更糟,因為讀者無從分辨。第二條測試會檢查線上算繪後的頁面,確保語言切換確實會跳轉。
Windows 請用 python -X utf8,pytest 輸出才不會亂碼。
Demo 截圖
python -X utf8 tools/shoot_screenshots.py依當前版本重新產生六張 demo 截圖,對應影片腳本的每一幕(初始看板、故障、重排方案、確認卡、已套用、身分切換)。它會用 ephemeral port 自行起伺服器,所以不會干擾正在跑的 demo。
它會寫兩份:docs/screenshots/(納入版控 —— README 顯示的、評審看到的)與 web/(當次擷取,gitignored —— 你的錄影素材)。同時還會寫 docs/screenshots/manifest.json,記錄每一幀顯示的文字;tests/test_screenshots_current.py 會重跑同樣的場景並比對文字,所以已提交的截圖不可能在無聲無息中過期。
開發文件
docs/friction-log.md—— 7 條得來不易的結論(只能用 SSE 的 elicitation、callback 必須 async、Context.request_context.request.headers、引擎可見性修正、SER_FROZEN_NOW凍結時鐘)。docs/product-feedback.md—— 對 MCP 生態的觀察(elicitation 的落差、json_response 的陷阱)。docs/dev-notes/COLLAB/CONTRACT-FROZEN.md—— 給夥伴的契約;docs/dev-notes/COLLAB/OUTBOX.md有客戶端接線指南。內部筆記在docs/dev-notes/。原本的tools/_deep4.py到tools/_deep8.py探針現在在archive/。tools/check_readme_i18n.py—— 確保本檔中英兩節內容一致(程式碼區塊、圖片、識別字、數字)。改動任一邊後請執行;tests/test_readme_i18n.py會把它納入測試套件。
技術選型
元件 | 選擇 | 原因 |
MCP SDK |
| 官方、支援 2025-11-25 規格與 elicitation |
傳輸 | Streamable HTTP(SSE 回應) | 伺服器→客戶端 elicitation 的必要條件 |
資料庫 | SQLite( | 交易式版本控制 + 審計紀錄,零維運 |
敘事 | Amazon Nova Lite,Bedrock Converse | 請求帶了 |
授權
MIT —— 見 LICENSE。示範資料為虛構資料;未做生產環境強化。
This server cannot be deployed
Maintenance
Related MCP Connectors
Remote MCP for TehProf Booking: browse resources & availability, create & cancel reservations.
Merged free/busy, find mutual time, propose bookings with human approval. Never event contents.
Collision-first MCP diagnostics for MCP, API, webhook and agent failures. Returns structured evidence, bounded repair guidance, agent compatibility checks and safe recovery routing.
AI workflow/MCP implementation package planner.
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceEnables explainable household replanning by generating reviewable schedule proposals from constraint solving, with transactional MCP tools for retrieving state, proposing changes, committing approved plans, and undoing previous plans.MIT
- AlicenseNot gradedqualityCmaintenanceEnables a local MCP host to read, plan, and recover a shared household departure schedule around people, a shared bathroom, completed tasks, and a fixed deadline after an interruption. It exposes tools to preview progress or duration changes and compare household-supplied recovery alternatives, with the exact stored draft applied only after explicit review.MIT
- AlicenseNot gradedqualityBmaintenanceEnables interruption-aware, same-day study planning through local MCP tools for managing tasks, fixed commitments, scheduling windows, replanning after interruptions, previewing and approving simulated calendar changes, and persisting or exporting a workspace.MIT
- AlicenseNot gradedqualityBmaintenanceEnables an agent to ingest a school late-start or closure notice, determine which family commitments it breaks, build a constraint-checked recovery plan, apply it only after a parent approves, and read back every change to confirm the family's morning is ready.MIT