Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5
groups:
go-deps:
patterns: ["*"]

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5
groups:
actions:
patterns: ["*"]
62 changes: 55 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,68 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version-file: go.mod
- run: go vet ./...
- run: go test -race -count=1 -v ./...
- name: Run tests with coverage
run: go test -race -count=1 -covermode=atomic -coverprofile=coverage.out -v ./...
- name: Coverage summary
run: go tool cover -func=coverage.out | tail -n 1

lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version-file: go.mod
- uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee # v9.2.1
with:
version: v2.12.2

smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version-file: go.mod
- uses: golangci/golangci-lint-action@v6
- name: Run end-to-end MCP smoke test
run: make smoke

vuln:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version-file: go.mod
- name: govulncheck (Go module CVE scan)
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
- name: osv-scanner (OSV.dev cross-ecosystem scan)
uses: google/osv-scanner-action/osv-scanner-action@9a498708959aeaef5ef730655706c5a1df1edbc2 # v2.3.8
with:
scan-args: |-
--recursive
./

actionlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
version: latest
persist-credentials: false
- name: Lint GitHub Actions workflows
uses: raven-actions/actionlint@205b530c5d9fa8f44ae9ed59f341a0db994aa6f8 # v2.1.2
11 changes: 6 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,23 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0
persist-credentials: false

- uses: actions/setup-go@v6
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version-file: go.mod

- name: Install syft
uses: anchore/sbom-action/download-syft@v0
uses: anchore/sbom-action/download-syft@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # v0.20.6

- name: Install cosign
uses: sigstore/cosign-installer@v3
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2

- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v7
uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0
with:
distribution: goreleaser
version: latest
Expand Down
4 changes: 3 additions & 1 deletion .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ project_name: mcp-opa

before:
hooks:
- go mod tidy
# `verify` checks checksums without mutating go.mod / go.sum, so a
# release stays bit-reproducible. CI / dev should run `go mod tidy`.
- go mod verify

builds:
- id: mcp-opa
Expand Down
29 changes: 29 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
BIN := mcp-opa
PKG := ./...

.PHONY: build test vet lint smoke clean fmt tidy

build:
go build -trimpath -ldflags "-s -w" -o $(BIN) .

test:
go test -race -count=1 -v $(PKG)

vet:
go vet $(PKG)

lint:
golangci-lint run $(PKG)

smoke: build
./scripts/smoke.sh ./$(BIN)

fmt:
gofmt -s -w .

tidy:
go mod tidy

clean:
rm -f $(BIN)
rm -rf dist/
95 changes: 58 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,69 +2,90 @@

[![ci](https://github.com/0-draft/mcp-opa/actions/workflows/ci.yml/badge.svg)](https://github.com/0-draft/mcp-opa/actions/workflows/ci.yml)
[![Go Reference](https://pkg.go.dev/badge/github.com/0-draft/mcp-opa.svg)](https://pkg.go.dev/github.com/0-draft/mcp-opa)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)

A [Model Context Protocol](https://modelcontextprotocol.io) server that lets an LLM agent run [Open Policy Agent](https://www.openpolicyagent.org/) Rego evaluations as a tool.
[MCP](https://modelcontextprotocol.io) server that exposes [OPA](https://www.openpolicyagent.org/) Rego evaluation as a tool an LLM agent can call.

Useful for "let Claude reason about a policy" workflows: paste a Rego module + an input doc into a chat and ask the model to walk through what happens. The model writes the query; `mcp-opa` runs OPA and returns the decision set.

## Tool

### `evaluate_policy`

Evaluate a Rego module against an input document and optional `data` namespace.

| Parameter | Required | Description |
| ------------ | -------- | ---------------------------------------------------------------------------------------- |
| `rego` | yes | Rego source code. Must include a `package` declaration. |
| `query` | yes | Query to evaluate, e.g. `data.example.allow` or `data.example.violations[_]`. |
| `input_json` | no | JSON-encoded input document (becomes the `input` variable inside Rego). |
| `data_json` | no | JSON-encoded base document seeding the `data` namespace via OPA's in-memory store. |

The tool returns the OPA `ResultSet` as JSON.
Paste a Rego module and an input doc into a Claude / Cursor session. The model picks the query, `mcp-opa` runs `opa.Eval`, returns the decision set.

## Install

```bash
go install github.com/0-draft/mcp-opa@latest
```

Or grab a signed binary from the [releases page](https://github.com/0-draft/mcp-opa/releases).
Pre-built signed binaries are on the [releases page](https://github.com/0-draft/mcp-opa/releases).

## Use with Claude Code
## Quickstart

```bash
claude mcp add opa -- mcp-opa
# Build and run the smoke test (no MCP client needed).
make smoke
# → ✓ smoke: allow=true
```

Then in a Claude Code session:
`make smoke` builds the binary, feeds it a synthetic MCP `initialize` →
`tools/list` → `tools/call` sequence over stdio, and asserts the policy
returned `allow=true`. It exits non-zero on protocol failure.

> Evaluate this RBAC policy against the request — does Alice get to delete the document?
>
> ```rego
> package rbac
> default allow := false
> allow if input.user == "alice" and input.action == "read"
> ```
## Wire it to Claude Code

Claude calls `evaluate_policy` with the right query, returns the decision.
```bash
claude mcp add opa -- mcp-opa
```

## Use with Cursor / other MCP clients
Then in a session: paste a Rego policy, an input doc, and ask the model what
the decision should be. The model calls `evaluate_policy`, gets the answer
from OPA (not from its training data).

Add to your client's MCP server config:
## Wire it to Cursor / other clients

```json
```jsonc
{
"mcpServers": {
"opa": {
"command": "mcp-opa"
}
"opa": { "command": "mcp-opa" }
}
}
```

## Verifying a release
## Tool: `evaluate_policy`

| Param | Required | Description |
| ------------ | -------- | -------------------------------------------------------------------------- |
| `rego` | yes | Rego source with a `package` declaration. |
| `query` | yes | Rego query, e.g. `data.example.allow`. |
| `input_json` | no | JSON-encoded `input` document. |
| `data_json` | no | JSON-encoded base document for the `data` namespace. |

Returns the OPA `ResultSet` as JSON.

## Examples

[`examples/`](./examples/) has reference policies:

- [`rbac.rego`](./examples/rbac.rego) — role → permission mapping
- [`abac.rego`](./examples/abac.rego) — clearance level comparison
- [`k8s_admission.rego`](./examples/k8s_admission.rego) — admission control: required labels

## Layout

Flat on purpose. A single-binary MCP server with one tool does not need
`cmd/`, `internal/`, or `pkg/`. When a second tool joins, the natural split is
a sibling file (`evaluate.go`, `lint.go`, ...) — still no subpackages.

```text
.
├── main.go # server bootstrap + tool registration
├── main_test.go
├── examples/ # reference Rego policies
├── scripts/smoke.sh
├── .goreleaser.yml
└── .github/
```

## Verify a release

Each release ships a `cosign`-signed checksum file (keyless, Sigstore via GitHub OIDC) and a CycloneDX SBOM. To verify before installing:
Releases ship a `cosign`-signed checksum file (Sigstore keyless via GitHub OIDC) and a CycloneDX SBOM per archive.

```bash
TAG=v0.1.0
Expand Down
30 changes: 30 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Examples

Reference Rego policies you can paste into `evaluate_policy` to see how `mcp-opa` behaves.

| File | Pattern |
| ------------------------------------------ | --------------------------------------------- |
| [`rbac.rego`](./rbac.rego) | Role-based: roles → permissions |
| [`abac.rego`](./abac.rego) | Attribute-based: subject + resource matching |
| [`k8s_admission.rego`](./k8s_admission.rego) | Kubernetes admission control (require labels) |

## Running an example end-to-end

```bash
# 1. Start mcp-opa from any MCP client config (Claude Code shown):
claude mcp add opa -- mcp-opa

# 2. In a session, ask:
# "Evaluate examples/rbac.rego for alice trying to delete document doc-1.
# What's the query?"
#
# Claude will read the rego, pick `data.rbac.allow`, send input
# {"user": "alice", "action": "delete", "resource": "doc-1"},
# and read back the decision.
```

You can also drive it manually over stdio (advanced; MCP is JSON-RPC over stdin/stdout):

```bash
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | mcp-opa
```
19 changes: 19 additions & 0 deletions examples/abac.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package abac

# Attribute-based access control: subject + resource attributes must align.
#
# Query: data.abac.allow
# Input: {
# "subject": {"id": "alice", "clearance": "secret"},
# "resource": {"id": "report-99", "classification": "secret"},
# "action": "read"
# }

default allow := false

levels := {"public": 0, "internal": 1, "confidential": 2, "secret": 3}

allow if {
input.action == "read"
levels[input.subject.clearance] >= levels[input.resource.classification]
}
17 changes: 17 additions & 0 deletions examples/k8s_admission.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package k8s.admission

# Kubernetes admission control: every Pod must carry an `app` label.
#
# Query: data.k8s.admission.violations[_]
# Input: the AdmissionReview request (or just the Pod object's metadata)

violations contains msg if {
input.kind.kind == "Pod"
not input.object.metadata.labels.app
# Use object.get with fallbacks so an undefined name/namespace doesn't make
# the whole sprintf evaluate to undefined and silently swallow the
# violation.
namespace := object.get(input.object.metadata, "namespace", "default")
name := object.get(input.object.metadata, "name", "<unnamed>")
msg := sprintf("Pod %s/%s is missing required label `app`", [namespace, name])
}
Comment thread
kanywst marked this conversation as resolved.
26 changes: 26 additions & 0 deletions examples/rbac.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package rbac

# Role-based access control.
#
# Query: data.rbac.allow
# Input: {"user": "alice", "action": "read", "resource": "doc-1"}

default allow := false

roles := {
"alice": {"editor"},
"bob": {"viewer"},
"carol": {"viewer", "auditor"},
}

permissions := {
"editor": {"read", "write", "delete"},
"viewer": {"read"},
"auditor": {"read", "audit"},
}

allow if {
some role in roles[input.user]
some perm in permissions[role]
perm == input.action
}
Loading
Loading