Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

koda-mcp-access

Reusable FastMCP access-control middleware for KODA-ecosystem MCP servers, plus identity helpers for tools. It is a pure Policy Enforcement Point (PEP): it reads identity + an already-resolved permission subset from proxy-injected headers and allows / denies / filters MCP tools, resources and prompts by pattern matching.

It does not know about roles, servers, Okta, or the permissions model — that intelligence lives upstream (usrv-koda = data, koda-proxy = subset resolution). Any MCP behind a proxy that honors the header contract can use it unchanged.

How it fits

client → registry/nginx → koda-proxy → AgentCore runtime → your MCP + KodaAccessMiddleware
                          (resolves &                        (this package:
                           injects X-Koda-* )                 reads & enforces)
  • Decision (who are you, what may you do) happens upstream in the koda-proxy: it calls usrv-koda, resolves the per-server subset, and injects it as base64-JSON in the permissions header.
  • Enforcement (allow / deny / filter each call) happens here. This package never validates the JWT, never queries a DB, never knows roles.
  • Trust: with signed identity on, the runtime only reads headers koda-proxy signed for it.

Components (C3)

What runs where, and which step each component owns:

 MCP client (Claude, Cursor, …)
   │  POST /mcp-koda/mcp   Authorization: Bearer <Okta JWT>
   ▼
┌─ mcp-gateway-registry ───────────────────────────────────────────────────────┐
│ nginx                                                                        │
│   ├─ auth_server /validate     JWT valid? groups                             │
│   └─ X-Koda-Server-Name = mcp-koda       (server path; the client cannot     │
│      X-Koda-Runtime-Arn = proxy_pass_url  set these, nginx overwrites them)  │
│                     │                                                        │
│                     ▼                                                        │
│ koda-proxy                                                                   │
│   1 verify the bearer (auth_server)                                          │
│   2 (server, ARN) is a registered pair ──────────► registry /api/servers     │
│   3 subset = permissions.mcp["mcp-koda"] ────────► usrv-koda profile API     │
│   4 build Koda-* headers, sign with k_mcp-koda ─► Secrets Manager            │
│                                                    .../mcp-identity          │
│   5 SigV4 InvokeAgentRuntime(ARN)                                            │
└─────────────────────┬────────────────────────────────────────────────────────┘
                      ▼
┌─ AgentCore runtime (one per MCP) ────────────────────────────────────────────┐
│ AgentCore forwards only the allowlisted ...-Custom-Koda-* headers            │
│                     │                                                        │
│                     ▼                                                        │
│ KodaAccessMiddleware (this package)                                          │
│   6 signature: audience, method, tool, ts ──► Secrets Manager                │
│                                               .../mcp-identity (same one),   │
│                                               derives k_mcp-koda             │
│   7 HeaderContract → Identity + MCPPermissions                               │
│   8 matcher: gate tools/call, resources/read, prompts/get; filter the lists  │
│   9 access log line + audit_hook                                             │
│                     │                                                        │
│                     ▼                                                        │
│ your tools ── get_current_identity() / get_current_access_token()            │
└──────────────────────────────────────────────────────────────────────────────┘

Steps 1–5 decide, steps 6–9 enforce. Step 6 runs with signature_mode verify (the default once KODA_SERVER_NAME is set) or require; with off (no server name) the runtime trusts the headers as they arrive, like 1.2.0 did.

Example: Ana calls mcp-koda

Ana has role qa, which in usrv-koda grants:

permissions: { mcp: { "mcp-koda": { tools: ["greeting", "jira_*"] } }, skills: [...], agents: [...] }

For tools/call greeting, koda-proxy sends this to the mcp-koda runtime (prefix X-Amzn-Bedrock-AgentCore-Runtime-Custom-Koda- omitted):

Sub:          ana@kushki
Roles:        qa
Permissions:  base64({"tools": ["greeting", "jira_*"], "skills": [...], "agents": [...]})
Client-Id:    0oa1koda
Signed-At:    1760400000
Key-Id:       2026-10
Signature:    v1=Xq3…kA.9f2c…   # <request_id>.HMAC(k_mcp-koda, v1 · ts · mcp-koda · tools/call · greeting · request_id · sha256(body) · headers)

What the middleware does with each request (runtime in require):

Request Result access line
tools/list greeting, jira_create, jira_get; my_profile is hidden target=* decision=allow
tools/call greeting runs; inside, get_current_identity().sub == "ana@kushki" target=greeting decision=allow
tools/call my_profile AuthorizationError: not in tools target=my_profile decision=deny
Someone with InvokeAgentRuntime sends Permissions={"tools":["*"]}, no signature denied before any header is read sub=- decision=deny reason=missing-signature
Ana's greeting signature replayed on tools/call my_profile denied: the signature covers the tool name reason=bad-signature
A signature made for another runtime (infra-mcp) denied: different key and audience reason=bad-signature
Registry scan: tools/list + Discovery: true, no bearer (koda-proxy signs it too) full catalogue bypass=discovery
tools/call + Discovery: true sent straight to the runtime denied for the missing signature; with off/verify the flag is ignored (WARNING authz: discovery flag ignored) and it is denied for the missing identity reason=missing-signature / reason=missing-identity

Wiring a runtime

from fastmcp import FastMCP
from koda_mcp_access import GuardConfig, KodaAccessMiddleware, get_current_identity

mcp = FastMCP("mcp-koda")
# Reads KODA_SERVER_NAME → verify, keyring from ai-projects/koda-platform/mcp-identity
mcp.add_middleware(KodaAccessMiddleware(GuardConfig()))


@mcp.tool()
async def greeting() -> str:
    return f"Hola, {get_current_identity().email}"
Where What
pyproject.toml koda-mcp-access>=1.8.0,<2
AgentCore runtime allowlisted_headers add ...-Custom-Koda-Signed-At, -Key-Id, -Signature, -Client-Id; for v2 signatures (1.8.0) also -Caller-Type, -Caller-Agent, -Request-Id
runtime role secretsmanager:GetSecretValue on ai-projects/koda-platform/mcp-identity in the runtime's region (plus kms:Decrypt if it uses a CMK)
runtime env KODA_SERVER_NAME (the registry path, lowercase, no /: mcp-koda-dev for /mcp-koda-dev)
runtime env, optional KODA_SIGNATURE_MODE (off / verify / require, default verify with a server name), KODA_IDENTITY_SIGNING_SECRET_ID (default ai-projects/koda-platform/mcp-identity), KODA_RESOURCE_AUTHZ=legacy (temporary, see Resources and prompts)

KODA_SERVER_NAME is the audience and the derive_for of the key: it must be the name koda-proxy sends as X-Koda-Server-Name, or every signature fails (reason=bad-signature). The secret is read by name, so each runtime gets the replica in its own region. At startup the middleware loads the keyring once and logs authz: signature_mode='verify' audience=mcp-koda-dev keyring=ai-projects/koda-platform/mcp-identity kid=…, or the error if the role cannot read it.

The signing secret is one for the whole platform, in each region where a reader runs (koda-proxy and every runtime; Secrets Manager replication keeps the copies equal):

{"kid": "2026-10", "key": "<32 random bytes, base64>"}

Rollout: set KODA_SERVER_NAME → verify (an invalid signature is denied, a missing one only logs WARNING authz: signature missing on <method>) → 24–48 h with no signature missing lines → KODA_SIGNATURE_MODE=require. KODA_SIGNATURE_MODE=off turns it off.

Install

uv add koda-mcp-access
# or with the optional standalone Okta verifier (local-dev):
uv add "koda-mcp-access[okta]"
# or: pip install koda-mcp-access
# release candidate (pip skips pre-releases unless pinned):
uv add "koda-mcp-access==1.8.0rc1"

Pin it in your project as:

# pyproject.toml
koda-mcp-access>=1.8.0,<2

Use

from fastmcp import FastMCP
from koda_mcp_access import KodaAccessMiddleware

mcp = FastMCP("My MCP")
mcp.add_middleware(KodaAccessMiddleware())   # defaults: AgentCore prefix, fail-closed

With an audit hook for observability (every allow/deny decision):

from koda_mcp_access import GuardConfig, KodaAccessMiddleware

def audit(d):
    logger.info("access action=%s target=%s decision=%s roles=%s",
                d.action, d.target, d.decision, d.identity.roles if d.identity else [])

mcp.add_middleware(KodaAccessMiddleware(GuardConfig(audit_hook=audit)))

What is enforced. The middleware gates tools/call, resources/read and prompts/get and filters their lists against the caller's grants: tools by tools, resources by resources or by the tools that link them as their UI, prompts by prompts (see Resources and prompts). Skills are authorized upstream in the registry, not here.

Read identity from inside a tool:

from koda_mcp_access import get_current_identity

@mcp.tool()
async def whoami() -> dict:
    ident = get_current_identity()
    return {"sub": ident.sub, "roles": ident.roles, "tenant": ident.tenant_id}

Tools that call a backend on the caller's behalf can get the raw JWT as a FastMCP AccessToken (or a compact user dict):

from koda_mcp_access import get_current_access_token, get_current_user

@mcp.tool()
async def list_my_initiatives() -> list:
    token = get_current_access_token()        # None if unauthenticated
    return await backend.get("/initiatives", bearer=token.token)

Standard: every KODA-ecosystem MCP authorizes with a single add_middleware(KodaAccessMiddleware()) and no local src/auth/. If a tool needs identity, import it from this package. See docs/MCP_ACCESS_STANDARD.md.

Standalone local-dev (validate the Okta JWT itself, requires [okta] extra):

from koda_mcp_access.verifiers.okta import OktaTokenVerifier
from fastmcp.server.auth import RemoteAuthProvider

if IS_LOCAL:
    mcp.auth = RemoteAuthProvider(
        # issuer: env OKTA_ISSUER; audience: env OKTA_AUDIENCE, no default
        token_verifier=OktaTokenVerifier(audience=OKTA_AUDIENCE),
        authorization_servers=[OKTA_ISSUER],
        base_url=MCP_BASE_URL,
    )

The audience has no default: without audience= or OKTA_AUDIENCE the constructor raises ValueError. It no longer falls back to OKTA_CLIENT_ID or api://default (KODA-32), so a token minted for another API is rejected.

Header contract

The upstream proxy injects, under a configurable prefix (default x-amzn-bedrock-agentcore-runtime-custom-koda-):

Header suffix Meaning
sub caller subject (required; absent → no identity)
email caller email (defaults to sub)
roles space-separated roles (resolved upstream)
auth-groups space-separated Okta groups
tenant-id tenant
authorization raw Okta JWT (for tools calling backends)
permissions base64(JSON) — the already-resolved subset for this MCP
discovery "true" → registry catalogue scan: list operations skip the filter; ignored everywhere else
client-id OAuth client the caller used (Okta cid); shows up as client= in the access log
signed-at, key-id, signature koda-proxy's signature over the headers above (see Signed identity context)

permissions decodes to a flat object — the server-scoped tools plus the global catalogs skills / agents, and optionally the server-scoped resources (URI patterns) and prompts (prompt-name patterns):

{ "tools": ["my_profile"],
  "skills": ["pm_challenge"], "agents": ["roadmap-agent", "koda-qa-test-design"],
  "resources": ["data://koda/*"], "prompts": ["welcome"] }

resources and prompts are optional, and a missing key is not the same as [] (see Resources and prompts).

Behavior

Operation perms Result
tools/call match in tools execute / AuthorizationError
tools/list tools filtered list
resources/read match in resources, or the UI resource of a tool allowed by tools read / AuthorizationError
resources/list, resources/templates/list same rule filtered list
prompts/get match in prompts render / AuthorizationError
prompts/list prompts filtered list (empty without prompts)
any other request (completion/complete, …) — passthrough with identity, fail-closed without it
initialize — passthrough
tools/list, resources/list, resources/templates/list discovery passthrough (full catalogue, no identity bound)
tools/call, resources/read, prompts/get, prompts/list, any other request discovery flag ignored (WARNING), normal identity + permissions check
any stdio passthrough (no filter)
any header missing/invalid + not stdio AuthorizationError (fail-closed)
any but initialize / ping signature invalid (verify/require) or missing (require) AuthorizationError, before any header is trusted

Skills are authorized upstream in the registry, not here.

Discovery only lists. The discovery header lets the registry scan the catalogue. It never lets a request execute a tool or read content: on tools/call, resources/read, prompts/get and every other request it is ignored.

Resources and prompts

Since KODA-32 resources and prompts are authorized like tools. Before, any caller with an identity read every resource and prompt.

A caller reads a resource when:

  1. a pattern in its resources grant matches the URI, or
  2. the URI is the UI resource of a tool it may call (its tools grant): the tool's _meta.ui.resourceUri, which @mcp.tool(app=AppConfig(resource_uri=...)) writes (the flat _meta["ui/resourceUri"] of earlier MCP Apps drafts counts too).
resources in permissions Resources the caller reads and lists
missing (what koda-proxy sends today) only the UI resources of the tools it may call
["data://koda/*", …] the matching URIs, plus the UI resources of its tools
[] none, the UI resources of its tools included

Rule 2 keeps MCP Apps views working without a resources grant: whoever may call get_brd_by_id reads ui://koda/brd_view.html, and nobody else does. To resolve it, the middleware lists the server's tools (without the middleware chain) once per resources/read or list. A resource no tool links and no grant matches is denied (not allowed to read resource '<uri>', decision=deny in the access log).

resources/templates/list shows a template when a grant or a linked UI URI could be one of its URIs: the pattern's fixed part (before a trailing *) and the template's (before the first {) agree, one starting with the other. It may show a template whose URIs the caller cannot all read; each resources/read is still checked on its own. To grant a template's URIs, use a prefix pattern (ui://koda/report/*).

Prompts are matched by name against prompts, with the same patterns as tools. Without prompts, or with [], every prompt is denied and prompts/list is empty: no producer sends prompts yet.

Rollout escape hatch. KODA_RESOURCE_AUTHZ=legacy (or GuardConfig(resource_authz="legacy")) brings back the 1.4.1 behavior for resources and prompts: identity required, no grant checked, lists unfiltered. The middleware logs WARNING authz: resource_authz='legacy' … at startup. Tools are enforced either way. It is temporary: drop it once the runtime's views and prompts work in enforce (the default).

Access log

Every operation logs one line on the koda_mcp_access.middleware logger: who came in and what they used. Allows go to INFO; denials and fail-open calls go to WARNING.

INFO    access sub=ana@kushki roles=qa tenant=acme client=0oa1koda action=tools/call target=greeting decision=allow
WARNING access sub=ana@kushki roles=qa tenant=acme client=0oa1koda action=tools/call target=my_profile decision=deny
WARNING access sub=- roles=- tenant=- client=- action=tools/call target=greeting decision=deny reason=missing-identity
WARNING access sub=- roles=- tenant=- client=- action=tools/call target=greeting decision=deny reason=bad-signature
INFO    access sub=- roles=- tenant=- client=- action=tools/list target=* decision=allow bypass=discovery
  • action is the MCP method; target is the tool name, resource URI or prompt name (* for lists).
  • bypass (discovery, transport, fail_open) and reason (missing-identity, missing-permissions, or a signature reason: missing-signature, bad-signature, stale-signature, bad-timestamp, unknown-key, keyring-unavailable) appear only when they apply.
  • Signature denials log sub=-: unsigned headers are not trusted, not even for the log.
  • It never logs the bearer, the grants or the tool arguments. Whitespace and control characters in values are replaced with _ so a caller cannot forge log lines.
  • audit_hook receives the same decisions (with action = MCP method) if you need to ship them somewhere else. Turn the line off with GuardConfig(access_log=False).

Signed identity context

Without it, anyone allowed to call the runtime directly (InvokeAgentRuntime) can send made-up Koda-* headers. With it, koda-proxy signs them and the runtime only trusts what it signed. The signature proves who sent the headers and that nobody changed them; it does not encrypt them.

# env: KODA_SERVER_NAME=mcp-koda  →  verify with the shared secret
mcp.add_middleware(KodaAccessMiddleware(GuardConfig()))

# the same, explicit (arguments win over the env):
from koda_mcp_access import SecretsManagerKeyring

mcp.add_middleware(KodaAccessMiddleware(GuardConfig(
    signature_mode="require",  # off | verify | require
    signature_audience="mcp-koda",  # this runtime's server name
    signing_key_provider=SecretsManagerKeyring(
        "ai-projects/koda-platform/mcp-identity", derive_for="mcp-koda"
    ),
)))
signature_mode Missing signature Invalid signature
off (no server name, or KODA_SIGNATURE_MODE=off) ignored ignored
verify (default with a server name) allowed, WARNING authz: signature missing on <method> denied
require denied denied
  • Checked on every request but initialize / ping (and stdio), discovery included, before any Koda-* header is read.
  • Each runtime verifies with its own key, derived from the shared secret (derive_for), so a signature made for one runtime does not verify on another. The signature also covers the audience (server name), the MCP method, the tool name on tools/call and a timestamp (max_skew_s, default 300). It does not cover the resource URI or the prompt name yet: a captured resources/read signature can read another resource the same caller is granted, within the 5 minutes.
  • Whoever reads the secret can sign for any runtime. Grant GetSecretValue only to koda-proxy and the runtimes' roles.
  • SecretsManagerKeyring (the default provider) caches the secret 5 minutes and accepts the current and the previous kid, so keys rotate without a redeploy. Rotate in two steps: new key in previous first; after 5 minutes, swap it into key (old one in previous), so no runtime sees a kid it does not know yet. The secret is {"kid", "key", "previous": {"kid", "key"}}, key in base64.
  • require cannot be combined with fail_mode="open" (ValueError at startup).
  • AgentCore only forwards allowlisted headers: add ...-Custom-Koda-Signed-At, ...-Key-Id, ...-Signature and ...-Client-Id to the runtime, or require denies everything.

The contract (canonical message, key derivation, test vector) is in docs/MCP_ACCESS_STANDARD.md.

Pattern matching

Name grants (tools, prompts, skills, agents) are matched the same way (: and . are interchangeable separators). An empty / missing list always denies (fail-closed):

Pattern Matches
* anything
jira_* prefix (jira_create, jira_get, …)
my_profile exact

resources patterns are URIs, compared literally and case-sensitively, with no separator normalization (in a URI : and . are not interchangeable): *, a trailing-* prefix (ui://koda/*) or exact. The URI is the one FastMCP passes to the hook, str(AnyUrl(uri)) (lower-case scheme, percent-encoded), so write patterns in that form.

agents (1.8.0) is the list of agents the caller may use, by registry path (["roadmap-agent", "koda-qa-test-design"]), matched by is_agent_allowed like tools. Agents have no permissions of their own: when an agent acts for a user, only the user's permissions apply. The 1.5 map {name: {actions: [...]}} is still read and keeps the names whose actions allow invoke (invoke, *, inv*…). is_agent_action_allowed is deprecated (DeprecationWarning): invoke delegates to is_agent_allowed, any other action is denied.

Configuration (GuardConfig)

Field Default Purpose
header_prefix AgentCore custom prefix header namespace to read
fail_mode "closed" closed denies when identity/perms absent; open passes through (logs a WARNING at startup and on every unfiltered tools/call)
bypass_transports {"stdio"} transports that skip filtering (local dev)
discovery_suffix "discovery" header suffix that marks a catalogue scan (list operations only)
audit_hook None Callable[[AccessDecision], None] for every decision
access_log True one access log line per operation (see Access log)
signature_mode None off / verify / require (see Signed identity context); None reads env KODA_SIGNATURE_MODE, else verify with an audience and off without
signature_audience None this runtime's server name; None reads env KODA_SERVER_NAME; required unless off
signing_key_provider None Callable[[], SigningKeyring]; None builds SecretsManagerKeyring(<KODA_IDENTITY_SIGNING_SECRET_ID or ai-projects/koda-platform/mcp-identity>, derive_for=<audience>) unless off
max_skew_s 300 max age / clock skew of a signature, in seconds
resource_authz None enforce / legacy (see Resources and prompts); None reads env KODA_RESOURCE_AUTHZ, else enforce
auto_enroll_group None opt-in: Okta group to add role-less callers to (e.g. "koda-builder")
no_role_equivalents frozenset() roles that count as "no role" for auto-enrollment (e.g. {"stakeholder"})
okta_admin_credentials_provider None Callable[[], OktaAdminCredentials] for auto-enrollment, e.g. SecretsManagerOktaCredentials(secret_id); None reads env OKTA_MGMT_SECRET_ID (see Auto-enrollment)

Auto-enrollment (opt-in)

When auto_enroll_group is set, a caller with no role (empty roles) is added to that Okta group. This is the one path where the package calls the Okta Management API. It needs the Okta service app credentials (client id + RSA private key), read from AWS Secrets Manager at runtime (KO-379):

# env: OKTA_ISSUER=https://<tenant>.okta.com  OKTA_MGMT_SECRET_ID=<secret name or ARN>
mcp.add_middleware(KodaAccessMiddleware(GuardConfig(
    auto_enroll_group="koda-builder",
)))

# the same, explicit (the argument wins over the env):
from koda_mcp_access import SecretsManagerOktaCredentials

mcp.add_middleware(KodaAccessMiddleware(GuardConfig(
    auto_enroll_group="koda-builder",
    okta_admin_credentials_provider=SecretsManagerOktaCredentials("usrv-mcp-koda/secret-manager"),
)))

The secret is a JSON object; other keys in it are ignored, so the service's existing secret works as is:

{"OKTA_MGMT_CLIENT_ID": "0oa…", "OKTA_MGMT_PRIVATE_KEY": "{\"kty\":\"RSA\", …}"}

OKTA_CLIENT_ID / OKTA_PRIVATE_KEY are accepted as keys too (the names in usrv-mcp-koda/secret-manager and ai-projects/koda-platform/secrets); OKTA_MGMT_* wins when both are there. The private key is the JWK as a JSON string (a nested JSON object also works).

Where What
pyproject.toml koda-mcp-access[aws] (boto3)
runtime env OKTA_ISSUER (or OKTA_DOMAIN): Okta base URL. OKTA_MGMT_SECRET_ID: name or ARN of the secret (not sensitive)
runtime role secretsmanager:GetSecretValue on that secret (plus kms:Decrypt if it uses a CMK)

Where the credentials come from, first match wins:

  1. GuardConfig(okta_admin_credentials_provider=...): any Callable[[], OktaAdminCredentials].
  2. env OKTA_MGMT_SECRET_ID: a SecretsManagerOktaCredentials over that secret.
  3. Deprecated: the key in env, OKTA_MGMT_PRIVATE_KEY or OKTA_PRIVATE_KEY (with OKTA_MGMT_CLIENT_ID or OKTA_CLIENT_ID). Still works, but logs WARNING koda-mcp-access: Okta admin private key read from env … is deprecated (KO-379) once per process and emits a DeprecationWarning. Move the key to the secret and drop it from the env.
  • Read lazily: on the first admin-token request (not at import or startup), then only when the Okta token is renewed. The secret is cached 5 minutes (SecretsManagerOktaCredentials(secret_id, ttl_s=300)), so a rotated key is picked up without a redeploy. A failed read is not cached.
  • Errors name the secret and the missing keys, never a value (secret '…' has no OKTA_MGMT_PRIVATE_KEY or OKTA_PRIVATE_KEY). OktaAdminCredentials leaves the key out of its repr.

The service app must use private_key_jwt auth and hold the okta.groups.manage scope. A web-app client_id + client_secret cannot manage groups.

The write (PUT /api/v1/groups/{gid}/users/{uid}) is idempotent; enrollment is best-effort (failures are logged, never raised) and eventual — the new membership takes effect on the caller's next session, once the upstream authorizer/proxy re-resolves roles. Leaving auto_enroll_group=None (the default) keeps the package a pure PEP with zero Okta writes.

Note: only callers with no role at all (empty roles header) are enrolled; anyone carrying any role is left untouched.

Internals

One file per concern — all role/server-agnostic:

Module Responsibility
middleware.py the Middleware hooks (on_call_tool, on_list_tools, on_*_resource*, on_*_prompt*, on_request for everything else); binds identity per request, enforces, resets
headers.py HeaderContract — builds header names from the prefix, reads them, base64-decodes permissions
identity.py Identity dataclass + identity_from_headers
permissions.py MCPPermissions shape + parse_permissions (tolerant of malformed input; keeps resources / prompts only when sent)
backend_auth.py BackendAuth (httpx.Auth) for the MCP's calls to its backend: Bearer, or HMAC koda-origin-v1 when a signed agent calls without a bearer, BackendAuthError otherwise; see "Llamadas del MCP a su backend" in docs/MCP_ACCESS_STANDARD.md
matcher.py the pure pattern matchers; is_tool_allowed, is_resource_allowed, is_resource_template_allowed and is_prompt_allowed drive enforcement, is_skill_allowed / is_agent_allowed are exported helpers for consumers (is_agent_action_allowed is deprecated)
context.py contextvar holding the current Identity for the request
config.py GuardConfig, AccessDecision
signing.py signed identity context: canonical message, key derivation, SecretsManagerKeyring and its cached-secret base SecretsManagerSecret (stdlib only, so other components can vendor it)
enrollment.py opt-in auto-enrollment (Okta Management API); admin credentials via SecretsManagerOktaCredentials
verifiers/okta.py optional standalone Okta JWT verifier (local dev only; explicit audience)

Request flow inside on_call_tool: bypass if stdio → verify the signature (unless off) → resolve identity + perms from headers (the discovery flag is ignored here) → deny if tool not granted → call_next → reset identity (always).

KODA-30 / AR-01 status

As of 2026-10-14. The live checklist is in the KODA-30 plan (planes/koda-mcp-access/PLAN_KODA-30_DISCOVERY_BYPASS.md).

Covered Repo Branch
Discovery only skips the filter on list operations; prompts/* and unknown methods fail closed (Phase 1) koda-mcp-access main (PR #3)
Signed identity context, verified per runtime, verify by default when KODA_SERVER_NAME is set (shared secret by default); tool name in the signature, per-runtime keys, no require with fail_mode="open", client= in the access log koda-mcp-access fix/koda-30-identity-signature
koda-proxy signs the Koda-* headers with the destination runtime's key mcp-gateway-registry feature/koda-30-identity-signature
H1: koda-proxy checks the (server, runtime ARN) pair; only admins set a server's Koda runtime; one server per ARN mcp-gateway-registry feature/koda-30-identity-signature
H2: only admins publish an AgentCore URL; one agent per runtime, in the registry and in koda-proxy mcp-gateway-registry feature/koda-30-identity-signature

Pending:

  • Publish 1.4.0 and move the consumers to it: usrv-mcp-koda, usrv-koda-infra-mcp, usrv-mcp-koda-builder (and its sqs_consumer), qmetry-mcp.
  • The shared signing secret (ai-projects/koda-platform/mcp-identity) in each region.
  • Each runtime: allowlisted headers, GetSecretValue on that secret, then verify → require.
  • Least privilege on bedrock-agentcore:InvokeAgentRuntime (Phase 3).
  • Sign the agent path too (koda-agent-runtime trusts Koda-* headers the same way).
  • H3 (auto-enrollment on unverified identity), H4 (tags and Trusted Publishing), H5–H7 (low).

KODA-32 status

As of 2026-10-20. Resources and prompts are authorized here (see Resources and prompts); the Okta verifier has no default audience. Pending:

  • KODA_SIGNATURE_MODE=require on each runtime once verify shows no signature missing lines.
  • usrv-koda (role_permissions) and koda-proxy emitting resources / prompts in the subset. Until then only rule 2 (UI resources of allowed tools) applies.

Develop

uv sync --group dev
uv run pytest
uv run ruff check src/ tests/

Metadata

Release files for koda-mcp-access 1.8.0rc1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for koda-mcp-access 1.8.0rc1
File Size Uploaded Provenance Actions
koda_mcp_access-1.8.0rc1.tar.gz 193.7 kB Without provenance Details for koda_mcp_access-1.8.0rc1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for koda-mcp-access 1.8.0rc1
File Interpreter ABI Platform Actions
koda_mcp_access-1.8.0rc1-py3-none-any.whl Python 3 none any Details for koda_mcp_access-1.8.0rc1-py3-none-any.whl

Total release size: 249.4 kB

Release files / koda_mcp_access-1.8.0rc1.tar.gz

Download URL koda_mcp_access-1.8.0rc1.tar.gz
Size 193.7 kB
Tags Source
SHA-256 checksum
How to use checksums
85f29d511372d011b37fbda0007c34678e1a37ae7846fd1f2a906ad233eb7f33
BLAKE2b-256 checksum
How to use checksums
5516e2103ac0067bdb72af37b6777c98e359d2d195cab466e73c133d879285b4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / koda_mcp_access-1.8.0rc1-py3-none-any.whl

Download URL koda_mcp_access-1.8.0rc1-py3-none-any.whl
Size 55.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9bf47758e8f4c4b2b1701fe967d924c6de063f05ffb1b220269750186bd5db2a
BLAKE2b-256 checksum
How to use checksums
d27d552a74bbe2be91c9faf67e8b5443000e9574bbafa8733bedfde1134bd9bf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for koda-mcp-access 1.8.0rc1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page