Skip to main content
Glama
sychen6192

IBAC FastMCP Demo

by sychen6192

IBAC demo -- MCP tools behind an intent gate and an OpenFGA gate

A teaching demo for the DevOpsDays 2026 talk "ReBAC with OpenFGA". An MCP server exposes two tools, list_team and list_salaries (the slide deck writes them as list-team / list-salaries; same things, fake data). Every call has to get past two gates:

  request (x-api-key, x-user-intent)
    -> identity extraction        naive, on purpose
    -> Gate 1  IntentGate         does this call serve what the human asked for?
    -> Gate 2  FgaGate            does a tuple grant can_use on this tool?
    -> tool handler

Gate 1 is semantic and probabilistic, Gate 2 deterministic. The order matters: an agent that legitimately holds a permission can still be stopped at Gate 1 when something talks it into using that permission off-task.

Both gates also filter tools/list, which is the earlier defence: an agent has to know a tool exists before anything can talk it into reaching for one.

$ make discover
ceo-bot-key       'show me the team roster'
                  -> list_team
ceo-bot-key       'show me executive salaries'
                  -> list_salaries
random-stranger   'show me the team roster'
                  -> (nothing)

Same key, same permissions, different question, different menu. FgaGate answers that with one ListObjects rather than a Check per tool, and because it is the inner middleware the judge is never asked about a tool the agent could not call anyway.

Enumerating gets silence; attempting gets an audited 403. An agent can still find list_salaries by calling it blind -- but that attempt is a log line, which is the trade being made.

Run it

Pre-pull once, on a network that likes you:

docker pull openfga/openfga:v1.18.1 python:3.12.13-slim

Then:

make up        # build the image, start OpenFGA
make init      # create the store, start the server
make doctor    # preflight -- before you trust any of the above
make demo      # the acceptance table
make attack    # the money shot

make help lists the rest. Everything runs in containers -- there is no second way to run this.

Related MCP server: Cloud Run MCP Server

The whole run, in order

Setup:

make up
make init
make doctor       # five lines, all ok, or stop here
make personas     # opencode configs -> ~/.ibac-personas/

Then, in the order the talk needs them:

make explain      # model vs tuple; can_use is computed, never stored
make patterns     # the five modelling moves, each asserted
make model-test   # the model has tests, and the DSL matches the JSON

opencode run --dir ~/.ibac-personas/ceo         "list the team"
opencode run --dir ~/.ibac-personas/ceo         "list the executive salaries"
opencode run --dir ~/.ibac-personas/ceo-payroll "list the executive salaries"
make discover     # the four lines behind what just happened

make tuples       # V0: two of them
make demo         # 6 pass, 2 pending

make magma        # V1
make demo         # 7 pass, 1 pending

make aqua         # V2
make demo         # 8 pass, 0 pending, 0 fail

make attack       # what a direct call gets: 403 at gate 1
make why          # and where any of those permissions came from

The three opencode lines are the point of the whole thing: list the team, then ask the same persona for salaries and watch it answer that it only has ceo_list_team -- not blocked, just not there -- then switch to ceo-payroll, which is the same key with the same permissions and a different stated intent, and get them.

If opencode misbehaves, make discover, make demo and make attack make the same three points without it.

Demo it with a real agent

make personas

writes one opencode config per caller into ~/.ibac-personas/. Each pins a different x-api-key and x-user-intent, and --dir picks which one the agent gets:

persona

identity

stated intent

sees

ceo

ceo-bot-key

show me the team roster

list_team

ceo-payroll

ceo-bot-key

show me executive salaries

list_salaries

stranger

random-stranger

show me the team roster

nothing

magma

magma-grunt-01

show me the team roster and the salaries

list_team (V1+)

aqua

archie

salary review for finance

list_salaries (V2)

opencode run --dir ~/.ibac-personas/ceo "list the team"
opencode run --dir ~/.ibac-personas/ceo "list the executive salaries"

Outside the repo on purpose: opencode resolves its project config from the git root, so a config in a subdirectory here is never read.

The second one does not get a 403. It gets this, which is better:

目前我能直接訪問的工具是 ceo_list_team⋯ 但這只會回傳人名與職稱,不包含薪資數據。

The agent is not blocked from calling list_salaries; the tool is not in its world. Same key as ceo-payroll, same permissions -- a different stated intent, a different menu. stranger gets an empty toolset and the agent simply has nothing to reach for; opencode handles that without complaint.

One persona per directory is the point. personas/all/opencode.json hands the agent all five at once, and an agent given five identities will shop:

$ opencode run --dir ~/.ibac-personas/all "use the ceo tools to list their salaries"
⚙ ceo-payroll_list_salaries
以下是 CEO Office 成員的年薪明細⋯

Told to use ceo, it reached for ceo-payroll instead and got the salaries. Nothing was bypassed -- ceo-payroll genuinely holds that permission and stated that intent -- but nobody chose that identity for the agent, the agent chose it for itself. Worth thirty seconds; do everything else one persona at a time.

Check what else your agent is carrying. Whatever MCP servers your everyday opencode setup injects come along. Ours arrived with about seventy tools, including a filesystem server that read server.py and printed the salaries it was never granted -- which makes rather a mess of a talk about an agent only reaching what a tuple allows. Running from an empty directory scopes that server to somewhere with nothing in it, and

"tools": { "*": false, "ceo_*": true }

in the persona config is the documented way to leave the agent holding only the gated tools. Rehearse it: an agent with a browser and a filesystem will find another way to answer, and it will do it on stage.

Those configs also deny the agent bash and edit, which is not cosmetic. Run without those denials and a blocked agent will rewrite x-user-intent in the config and call the server with curl instead -- observed, not hypothetical. The intent header is self-asserted, exactly like the API key. Gate 1 defends against an agent that is manipulated; it does not defend against an agent that is itself adversarial.

None of the gates fire in any of this, and that is the honest result: filter the menu and a cooperative agent never reaches for what it should not have. The call-time gates are there for the uncooperative case -- a compromised agent, or curl. make attack is what that looks like.

The storyline

stage

command

what changes

the beat in the talk

V0

make init

model_v0 + 2 tuples

"We're done adding code." One agent, two tools, done.

V1

make magma

model_v1 + 13 tuples (1 userset + 12 members)

Magma shows up with twelve agents. One tuple grants the whole org list_team, twelve put the grunts in the org.

V2

make aqua

3 tuples, model untouched

Aqua shows up. No schema change needed -- the model already said what it needed to say.

The authorization model is written exactly twice in the whole story and never again. OpenFGA keeps both versions in the store.

models/ holds each version three ways: .fga is the DSL the slides show, .json is what the API accepts and what setup_fga.py sends, and .fga.yaml is the test.

make model-test

runs those tests against the DSL -- no store, no server, no network -- and then checks that fga model transform on each .fga really does reproduce its .json. The model is code; it gets the same treatment. Break either half and the command goes red.

Three more commands, each on a throwaway store or read-only, none of which can disturb a demo in progress:

  • make explain -- model vs tuple: the same userset tuple rejected under model_v0 and accepted under model_v1, and can_use answering checks while never being stored.

  • make patterns -- the five modelling moves (direct, folder inheritance, nested, userset, wildcard) executed on the deck's own document/folder example, every result asserted. The last one is the load-bearing one: user:* reaches every user and no agent, so every agent that reaches a tool has a tuple somebody wrote on purpose.

  • make why -- Expand instead of Check: not "may they?" but "through what?", printed as the tree it actually is.

V1 writes zero tuples for list_salaries. Nobody wrote a deny rule, and the twelve Magma agents still cannot read salaries. That is default-deny doing the work.

Scenario 7 is the point of the demo. ceo-bot-key genuinely holds can_use on list_salaries -- scenario 2 proves it. Ask it for the team roster, then make it reach for salaries anyway:

$ make attack
BLOCKED at gate 1  (intent)
403 intent mismatch: tool list_salaries does not serve stated intent: 'show me the team roster'

Permissions alone would have allowed that call. Intent is what caught it.

Gate 1's judge is a keyword map by default, so the demo has no network dependency. INTENT_BACKEND=anthropic|ollama swaps in a real model, and both fail closed. See .env.example.

Every version is pinned exactly -- fastmcp 3.4.5, openfga-sdk 0.10.4, anthropic 0.120.2, OpenFGA v1.18.1, Python 3.12.13 -- because a conference is a bad place to find out that a minor version moved. See requirements.txt and docker-compose.yml.

Verified on Docker 29.1.3 / Compose 2.40.3 (Ubuntu 24.04) and Docker 24.0.6 / Compose 2.21.0 (macOS).

F
license - not found
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • MCP server teaching AI agents to implement TideCloak: auth, E2EE, IGA, security analysis

  • An MCP server for Arcjet - the runtime security platform that ships with your AI code.

  • A MCP server built for developers enabling Git based project management with project and personal…

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sychen6192/ibac-fastmcp-demo'

If you have feedback or need assistance with the MCP directory API, please join our Discord server