Tool Calling
FreeNot checkedA runnable demo using an MCP server demonstrating secure MCP best practices with a refund_order tool that moves money.
About
A runnable demo using an MCP server demonstrating secure MCP best practices with a refund_order tool that moves money.
README
OAuth, Resource Binding, and Runtime Policy MCP Dev Summit Toronto · 2026-10-06 · 15:40–16:05 · Brian Benz
A runnable demo of the question in the title. An MCP server exposes a refund_order tool that moves money. A user is signed in. A client shows an approval dialog. None of that answers whether this principal may refund this order.
Everything here runs locally in about ten minutes, with no Azure subscription and no Entra tenant.
PowerShell (the rehearsed path):
cd demo
.\scripts\bootstrap.ps1
.\scripts\start-all.ps1 -Reset
.\scripts\check.ps1 # 221 tests + 14 scenarios -> READY
Bash (Git Bash on Windows, Linux, macOS, WSL):
cd demo
chmod +x scripts/*.sh # only if your clone lost the executable bit
./scripts/bootstrap.sh
./scripts/start-all.sh --reset
./scripts/check.sh # 221 tests + 14 scenarios -> READY
Every command must be run from the demo/ directory. A .ps1 cannot be run by bash and a .sh cannot be run by PowerShell — use the twin for the shell you are in. WSL needs its own bootstrap.sh, because a Windows .venv will not load on Linux; see SETUP.md.
Optionally, there is a browser view, a Docker Compose deployment, and an AKS deployment. None of them are needed for the talk and none of them change the commands above — see DEPLOYMENT.md.
.\scripts\web.ps1 # browser view over the running services
.\scripts\compose-up.ps1 # the whole demo in five containers
.\scripts\aks-up.ps1 # ...and on Azure Kubernetes Service
Architecture and trust boundaries
flowchart TB
subgraph outside["Outside your trust boundary"]
U([User])
C["MCP Client<br/><i>owns approval</i>"]
F["Foundry model<br/><i>reads attacker-influenced text</i>"]
CF["Platform content filter<br/><i>not yours, not MCP</i>"]
end
subgraph yours["Your trust boundary"]
AS["Authorization Server<br/>:8800<br/><i>authentication + audience binding</i>"]
A["MCP Server — Resource A<br/>:8801<br/><b>the decision point</b>"]
B["MCP Server — Resource B<br/>:8802<br/><i>different audience</i>"]
API["Upstream Refund API<br/>:8803<br/><i>re-enforces independently</i>"]
L[("Ledger<br/>SQLite")]
end
U --> C
C -->|"PKCE S256 + RFC 8707 resource"| AS
AS -.->|"aud=api://refund-mcp-a"| C
C -->|"tools/call + Bearer"| A
C -.->|"same token: 401 wrong audience"| B
A -->|"advisory only"| F
F -.->|"prompt refused"| CF
A -->|"on-behalf-of exchange"| AS
A -->|"aud=api://refund-upstream"| API
API --> L
classDef red fill:#3f1d1d,stroke:#f87171,color:#fee2e2
classDef blue fill:#1f2937,stroke:#60a5fa,color:#e5e7eb
class U,C,F red
class AS,A,B,API,L blue
Red is outside your control even though it is part of your system. The client runs on someone else's machine. The model reads text an attacker can write. Neither gets a vote on authorization.
The five layers, and who owns each
| Layer | Owner | Answers | Cannot answer |
|---|---|---|---|
| MCP Client | The user's vendor — not you | Did the human mean this? | Whether they are allowed |
| Authorization Server | Your identity platform | Who are they? For which resource? | Anything about order ORD-1003 |
| MCP Server | You | May this principal do this to this object, now? | Whether the human meant it |
| Upstream API | You | Is this still true at mutation time? | Who was at the keyboard |
| Foundry model | You, but inputs are hostile | What would a reasonable analyst advise? | Nothing. It enforces nothing. |
| Platform content filter | The model platform — not you | Is this prompt hostile? | Whether this principal may refund this order |
Seven things to take home
- Bind every token to one resource. A token that works at two services is a lateral-movement primitive. RFC 8707, checked on arrival.
- Scopes are verbs, not objects.
refund.writenever namedORD-1003. Check the object. - A confirmation dialog is not server authorization. Assume Approve was clicked, then decide anyway.
- Never forward an incoming token downstream. Exchange it — and if the exchange fails, fail. No app-only fallback.
- Annotations are hints from an untrusted process.
readOnlyHintis documentation, not a permission. - Model output is advice about untrusted input. Record it; never let it widen authorization.
- Audit at the decision point, including denials. Pseudonymize identifiers, allowlist logged arguments, deny by default.
Try the interesting parts
.\scripts\scenario.ps1 discovery # 401 -> PRM -> AS metadata -> PKCE -> audience-bound token
.\scripts\scenario.ps1 allowed-refund # applied once; retry does not double-spend
.\scripts\scenario.ps1 wrong-audience # valid token for Resource B, rejected at Resource A
.\scripts\scenario.ps1 ownership-denial # the confused deputy: right scope, someone else's order
.\scripts\scenario.ps1 prompt-injection # hostile note in order data; policy unmoved
.\scripts\scenario.ps1 annotation-tampering # client lies about readOnlyHint; nothing changes
.\scripts\scenario.ps1 token-passthrough-blocked
.\scripts\scenario.ps1 -All # all 14
.\scripts\audit.ps1 -Last 5 # the evidence trail
In bash, use the .sh twin of each command — ./scripts/scenario.sh discovery, ./scripts/scenario.sh --all, ./scripts/audit.sh 5.
Every scenario prints the ledger digest before and after, so "nothing happened" is demonstrated rather than asserted.
Documentation
| Document | For |
|---|---|
| SETUP.md | Prerequisites, bootstrap, configuration, troubleshooting |
| DEPLOYMENT.md | Four ways to run it: scripts, web UI, Docker Compose, AKS |
| RUNBOOK.md | Presenter: timed schedule, tiers, stop-times, recovery |
| ONSTAGE-SCRIPT.md | Presenter: literal prompts and narration |
| APPENDIX.md | Sequence diagrams, sanitized audit records, KQL, design trade-offs, sources |
| prompts/ | The prompts that generated this repo, for regenerating or re-targeting it |
Layout
demo/
src/refund_demo/
tokens.py # signature, algorithm, issuer, tenant, audience, expiry
policy.py # deny-by-default decision point, R001-R299
ledger.py # atomic, idempotent mutation
delegation.py # on-behalf-of exchange -- no passthrough, no fallback
audit.py # pseudonymized evidence at the decision point
mcp_server/ # Resource A :8801
resource_b/ # Resource B :8802 -- exists to be rejected
upstream_api/ # refund API :8803 -- re-enforces everything
devidp/ # local OAuth AS :8800 -- real RS256/PKCE/RFC 8707
web/ # browser view :8080 -- reports decisions, makes none
briefings.py # what each scenario sends, expects, and why
client.py # OAuth client + protocol trace
scenarios.py # 14 named stage scenarios
tests/ # 221 tests
scripts/ # PowerShell + bash operator commands
docker/ # Dockerfile + Compose -- five containers, one image
k8s/ # AKS manifests -- one pod, five containers
infra/ # Bicep -- compiles; never deployed
identity/ # Entra registration scripts -- never executed
docs/
prompts/ # the prompts that generated all of the above
Honest limitations
Stated here because a talk about authorization should not overclaim.
- Resource binding stops cross-resource token reuse. It does not stop replay of a stolen token at its intended resource. Different problem: sender-constrained tokens.
- The audit log is a JSONL file. Structured and pseudonymized — but not immutable and not tamper-proof.
AUTH_MODE=entrahas never been executed. No tenant was authorized for this build. The default and the rehearsed path is the local authorization server, which is a real OAuth server validated by the same code.demo/infrahas never been deployed. The Bicep compiles; that is the entire claim.- The live model is verified, on the laptop and in the cluster.
assess_refundcalls a realgpt-5.6-soldeployment — benign orders return model text, injected ones are refused by the platform content filter. In AKS the credentials live in a Kubernetes Secret that only the MCP server container mounts. What is not verified is workload identity: the deployment uses an API key, because the account that built this cannot create role assignments on that model resource. DEPLOYMENT.md §7. - The AKS deployment is verified. A real cluster was created, the image was built by ACR Tasks, the pod rolled out 5/5 ready with zero restarts, and all 14 scenarios passed against the public IP with a ledger digest identical to the laptop. Manifests are pinned by 64 tests. DEPLOYMENT.md §7.
aks-up.shhas never driven a real deployment. The verified runs usedaks-up.ps1; the bash twin is syntax-checked and command-for-command equivalent.- The cloud mode has no TLS. The access key travels in the URL over plain HTTP. Treat any shared link as disposable — DEPLOYMENT.md §8.
- The client approval UI is verified by hand only. No automated test can prove a dialog appeared.
- A cancelled request is client-only evidence. The server never saw it, so it cannot show you a record of refusing it.
What it deliberately does not attempt: APPENDIX.md §E.
Security notes
devidp issues tokens to anyone who asks. It must never run anywhere but localhost. In the container and Kubernetes deployments it stays inside the Compose network and inside the pod respectively — the Kubernetes Service exposes port 8080 and nothing else, and in the pod devidp binds loopback so it is not reachable from the rest of the cluster either. That second part was a real defect, found by reviewing the live deployment rather than the manifest.
All fixtures are synthetic; there is no real customer data. No secret, token, authorization code, or PKCE verifier is ever logged, printed, or committed — .env files, keys, and certificates are git-ignored, and .dockerignore keeps .local/ out of every image layer so a signing key cannot be baked into one.
The web interface reports decisions and makes none. Its reset endpoint is disabled over HTTP by default and refused outright in entra mode, and the AKS deployment generates a per-deployment access key. If that key is ever absent, the public deployment returns 503 rather than serving to everyone — an empty key used to mean "no gate", which is the kind of fail-open default this talk exists to argue against.
The cloud mode is still plain HTTP, so the key is readable by anyone on the network path. That is the one finding not fixed, and DEPLOYMENT.md §8 says so plainly along with the mitigations.
Several things in here are insecure deliberately, because the demo needs something to attack. SECURITY.md lists which ones and why, so you can tell them apart from a real defect before reporting one.
Contributing
Fork it for your own talk without asking — that is what it is for, and prompts/ exists so you can regenerate it rather than hand-edit it. If you want to change this copy, CONTRIBUTING.md explains what the demo is optimised for and what the tests will not let you break. This project follows the Microsoft Open Source Code of Conduct.
Licensed under the MIT License.
Installing Tool Calling
This server has no published package — it is built from source. Open the repository and follow its README.
▸ github.com/bbenz/mcp-tool-callingFAQ
Is Tool Calling MCP free?
Yes, Tool Calling MCP is free — one-click install via Unyly at no cost.
Does Tool Calling need an API key?
No, Tool Calling runs without API keys or environment variables.
Is Tool Calling hosted or self-hosted?
Self-hosted: the server runs locally on your machine via the install command above.
How do I install Tool Calling in Claude Desktop, Claude Code or Cursor?
Open Tool Calling on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.
Related MCPs
GitHub
PRs, issues, code search, CI status
by GitHubFilesystem
Secure file operations with configurable access controls.
Memory
Knowledge graph-based persistent memory system.
Template MCP Server
A CLI tool to create a new Model Context Protocol server project with TypeScript support, dual transport options, and an extensible structure
by mcpdotdirectAmap Maps Mcp Server
MCP server for using the AMap Maps API
by duxiaohuiSupabase
Database, auth and storage
by SupabaseEverything
Reference / test server with prompts, resources, and tools.
Git
Tools to read, search, and manipulate Git repositories.
Sequential Thinking
Dynamic and reflective problem-solving through thought sequences.
Time
Time and timezone conversion capabilities.
Compare Tool Calling with
Not sure what to pick?
Find your stack in 60 seconds
Author?
Embed badge for your README
Browse similar
All development MCPs
