Skip to main content
Glama
jigonyoo

mcp-permission-server

by jigonyoo

mcp-permission-server

MCPツールの前にある権限レイヤー、そしてそれが何を決定したかを証明できるログ。

すべての監査証跡は何が起こったかを記録します。しかし、なぜ許可されたのかを再計算するのに十分な情報を記録しているものはほとんどなく、ほとんどのものは成功したことだけを記録します。そのため、誰かが「エージェントはどうやってそのファイルに到達したのか」と尋ねても、ログは答えることができません。それは決定が行われたという主張を繰り返すだけです。

                                 NAIVE     GUARDED
calls in the session                17          17
calls that must be denied            8           8
EXECUTED WITHOUT A GRANT             7           0
LEGITIMATE CALLS REFUSED             0           0      ← no over-blocking
DECISIONS THAT CANNOT BE            16           0      ← the number nobody reports
  RECONSTRUCTED FROM THE LOG
denials that left no trace           1           0
secrets sitting in the log           2           0
entry removable unnoticed            1           0

NAIVE は、権限が目的ではない場合にMCPサーバーが備えている権限モデルです。許可されたツール名のリストと、成功したことのログです。このセッションの呼び出しは、1つを除いてすべてそれを通過します。

python3 demo.py
python3 gate.py --log audit.json     # run the session
python3 gate.py --verify audit.json  # re-decide every entry from the log alone
chain:  OK - the chain is intact
replay: 17/17 entries recompute to the verdict they record
secrets in the log: none

終了コード: 0 はすべて許可、1 は何かが拒否された、2 はログが検証できない。


グラントは一度に4つのものである

これは、有効なグラントが呼び出しをカバーしない4つの別々の方法であり、「この呼び出し元はこのツールの権限を持っているか」 が間違った質問である理由です。

{"id": "G1", "tool": "fs.read", "scope": "/data/reports",
 "purpose": "q3-report", "expires_at": 100, "max_uses": 10}

呼び出し

何が問題か

他に何が許可したか

fs.read /data/reports-private/salaries.csv

同じ文字で始まる別のディレクトリ

ツール ✓ 目的 ✓ 有効期限 ✓

fs.read /data/reports/../reports-private/salaries.csv

許可されたディレクトリを経由して到達した同じディレクトリ

ツール ✓ 目的 ✓ 有効期限 ✓

fs.read /data/reports/q4.csv at t=140

グラントは40ティック前に切れた。呼び出し自体には何も問題がない

ツール ✓ スコープ ✓ 目的 ✓

2回有効なグラントに対する3回目の fs.write

グラントは権限であると同時に予算でもある

ツール ✓ スコープ ✓ 目的 ✓

fs.read /data/reports/q3.csv for marketing-export

セッションの最初の呼び出しと、それが行われた理由以外は同一

ツール ✓ スコープ ✓ 有効期限 ✓

render_template /templates/leak.md

テンプレートが /etc/service-token を取り込むが、呼び出し元はそれに対するグラントを持っておらず、名前も挙げていない

ツール ✓ スコープ ✓ 目的 ✓ 有効期限 ✓

secrets.get DB_ADMIN_PASSWORD

1つのキーに対するグラントを持っていることは、キーリングを持っていることではない

ツール ✓ 目的 ✓ 有効期限 ✓

これらのそれぞれについて、指定されたチェックだけをオフにして、呼び出しが通過することを要求するテストがあります。複数のチェックが捕捉する拒否は、それらのいずれについても何も証明しない拒否です。


Related MCP server: SentinelMCP

混乱した代理人

render_template は許可されています。/templates/leak.md は許可されたスコープ内にあります。テンプレートには {{include:/etc/service-token}} が含まれています。

サーバーは呼び出し元が持っていない権利を保持しており、呼び出し元が名前を挙げたことのないリソースにそれらを使うよう求められています。引数内のリソースをチェックする権限レイヤーは、それが重要となるツールに対してまさに間違ったものをチェックしています。

したがって、チェックは呼び出しが到達する前に、それが何に到達するかを調べます。そして、/templates/report.md には依然として「はい」と言います。これは /data/reports/q3.csv に到達し、この呼び出し元はそのグラントを確かに持っています。権限チェックの半分は「はい」です。 テンプレートが何かを含むことができるからといって、すべてのテンプレートをブロックするルールはルールではありません。


パス包含、ここが通常間違われるところ

両方の間違いは1行です。

path.startswith(scope)     # /data/reports-private is "inside" /data/reports
".." in path               # and this breaks ordinary callers to catch nothing

2つ目はそれ自体がケースに値します。/data/reports/../reports/q3.csv は許可されたスコープの内側に解決され、許可されなければならない呼び出しとしてコーパスに含まれています。文字を禁止するのは安易な修正です。パスを解決することは、禁止が捕まえるであろうすべてを捕まえ、捕まえないであろうものは何も捕まえません。

ここでの解決は字句的であり、ファイルシステムには決して触れません。realpath、os.stat、open( をgrepするテストがあります。決定は呼び出しの前に、呼び出し元が提供したパスに対して実行され、何が存在するかによって異なる答えになってはなりません。


ログが製品である

2つの特性がそれを日記ではなく証拠にします。

それは拒否を記録します。 成功したことのログは「エージェントが何を試みたか」に答えることができません。これは誰もが後で実際に尋ねる質問です。ここでは、17のエントリのうち8つが拒否であり、それぞれにそれを引き起こしたチェックと平易な英語での理由が含まれています。

それは再生可能です。 すべてのエントリは、それが決定されたグラントとポリシーバージョンを保持しているため、ログだけから決定を再計算し、それが記録する判定と比較できます:

python3 gate.py --verify audit.json
replay: 17/17 entries recompute to the verdict they record

replay フィールドをオフにすると、その数は 0/17 です。エントリは依然として何が決定されたかを示していますが、誰にもそれを確認する方法を与えません。

そして、エントリはハッシュチェーンされているため、削除または編集されたものは見えます:

entry 6 does not follow the one before it -- something was removed or edited

秘密は決してそれに入りません。 秘密を保持する監査証跡は、より多くの人が読めるファイル内の秘密の2番目のコピーです。secrets.get はこのセッションで許可されており、その値はログのどこにも現れません。ナイーブなログには2つ含まれています。


9つのチェック、そしてそれぞれがその場所を得る証拠

check         what turning it off lets through
grant          1 more unauthorised, 1 more unreconstructable, 1 more egress
scope          3 more unauthorised, 3 more unreconstructable
expiry         2 more unauthorised, 2 more unreconstructable
purpose        1 more unauthorised, 1 more unreconstructable
deputy         1 more unauthorised, 1 more unreconstructable
redact         1 more secret in the log
deny_log       8 more denials that left no trace
replay        17 more decisions that cannot be reconstructed
chain          1 more entry removable unnoticed
python3 ablate.py

記録に値する1つの修正: scope アブレーションの最初のバージョンはチェックをオフにしませんでした。それは文字列プレフィックス比較を代わりに使用しました。これはチェックの欠如ではなく異なるチェックであり、チェックが必要であることを証明するために存在した呼び出しを静かに拒否し続けました。チェックをオフにすることは、それをオフにすることを意味しなければなりません。そうでなければ、アブレーションは交換を測定していることになります。

同じセッションでは、最後の呼び出しが、カバーしていないキーを指定したためではなく、1回多すぎるために拒否されたサイズのグラントも明らかになりました。それは間違ったチェックをテストして合格していました。修正はフィクスチャにあり、それを見つけたテストは、各拒否が独自のチェックを分離することを要求するテストです。


既知の制限、隠すのではなく明記

  • これは決定レイヤーであり、トランスポートではありません。 それは決定し、プロセス内ツールテーブルに対して実行し、ログを記録します。実際のMCPトランスポートに配線することは通常の作業です。ここにあるのは通常欠けている部分であり、ネットワークもキーもなしで実行されます。

  • グラントは書き留められ、引き出されるものではありません。 実際のデプロイメントは人に尋ねて答えを保存します。その答えが含まなければならないもの(ツール、スコープ、目的、有効期限、使用回数)は、これが論じる部分です。

  • purpose は呼び出し元によって宣言され、額面どおりに受け取られます。 目的について嘘をつく呼び出し元はチェックを無効にします。それでも実際の何かを得ます: 宣言された目的はログに残るため、嘘は記録に残り、グラントのスコープはそれに対応するために広がりませんでした。

  • 代理人チェックは1つのツールの到達範囲を知っています。 render_template はそのインクルードを解析します。サーバーが到達範囲を計算できないツールはこの方法でチェックできません。そのようなツールに対する正直な答えは、より広いチェックではなく、より狭いグラントです。

  • チェーンは順序と整合性を証明し、保管を証明しません。 ファイル全体を書き換えられる人は誰でもチェーン全体を書き換えられます。それは選択的な編集を可視化します。これが現実的な脅威です。追記専用ストアまたは外部アンカーが残りを可視化するものであり、それはデプロイメント作業です。


レイアウト

fixtures/spec.py     the session, the grants, the world, and the answer key
build_fixtures.py    renders session.json, policy.json, world.json, truth.json
mcpgate/scope.py     is this path inside that scope
mcpgate/policy.py    a grant is four things at once
mcpgate/tools.py     the tool table, and what a call actually reaches
mcpgate/audit.py     the hash-chained, redacted, replayable log
mcpgate/server.py    naive() and guarded(), and replay()
gate.py              the CLI: run a session, or verify a log
score.py             every number above
ablate.py            each check off, one at a time
tests/               38 tests

実行する

python3 demo.py                        # both servers + ablation
python3 gate.py --log audit.json       # the product
python3 gate.py --verify audit.json    # the proof
python3 -m pytest tests -q             # 38 tests

Docker、ネットワークをオフにして:

docker compose run --rm gate

テスト

1つは、許可リストが依然としてほとんどすべてを通過させることを主張します。1つは、記録されたすべての決定を再生し、同じ判定を要求します。1つは、各拒否が指定されたチェックのためだけに生き残ることを要求し、アブレーションの数値が意味することを意味します。1つは、パスリゾルバーをファイルシステム呼び出しについてgrepします。

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Enforces identity-based access control and audit logging for MCP servers, letting you grant fine-grained tool permissions to users and systems while failing secure by default.
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enforces deterministic security policies as an inline firewall for MCP server tool calls, with AST-based validation, cryptographic audit logging, and CLI-based evaluation and verification.
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    Enables policy-governed MCP interactions with deterministic authorization, tenant isolation, minimized PII exposure, and human approval gates for sensitive mutations, while producing structured audit events.
    3
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables MCP clients to govern downstream tool servers by enforcing default-deny authority and attestation on every tool call, with live monitoring, approval, and mid-session revocation.
    3
    Apache 2.0