Skip to content

feat: CapiscioMCPServer.connect() one-liner, decision cache, keeper domain#32

Merged
beonde merged 9 commits into
mainfrom
feat/connect-one-liner
May 13, 2026
Merged

feat: CapiscioMCPServer.connect() one-liner, decision cache, keeper domain#32
beonde merged 9 commits into
mainfrom
feat/connect-one-liner

Conversation

@beonde
Copy link
Copy Markdown
Member

@beonde beonde commented May 12, 2026

Summary

Adds CapiscioMCPServer.connect() — a one-liner factory that replaces the multi-step MCPServerIdentity + CapiscioMCPServer pattern. Also includes performance and DX improvements.

Changes

New API surface:

  • CapiscioMCPServer.connect(**kwargs) — reads env, creates identity, returns ready-to-use server
  • MCPServerIdentity.connect_sync() / from_env_sync() — sync wrappers for non-async callers

Performance:

  • 5-second decision cache in guard — keyed on (badge_jws, tool_name). Same badge string = same decision. First call ~3ms, subsequent ~0.01ms within a burst.

Bug fixes:

  • ServerBadgeKeeper now accepts domain parameter (required by registry for domain-scoped badges)
  • Badge renewal response parsing now checks token key (alongside badge)

DX:

  • Suppress gRPC C-core stderr noise (GRPC_VERBOSITY=NONE) at package import time
  • Downgrade noisy guard/PDP log levels (warningdebug/info)
  • Update all README examples and deployment docs to use connect() API

Related PRs

  • capiscio-docs: refactor/connect-api-rename
  • langchain-capiscio: refactor/connect-api-rename"

…er domain support

- Add CapiscioMCPServer.connect() class method — one-liner server setup from env
- Add MCPServerIdentity.connect_sync() / from_env_sync() for sync callers
- Add 5s decision cache in guard (eliminates repeat gRPC calls within bursts)
- Pass domain to ServerBadgeKeeper for badge renewal
- Look for 'token' key in badge renewal response (alongside 'badge')
- Suppress gRPC C-core stderr noise at package import time
- Downgrade noisy guard/PDP log levels (warning → debug/info)
- Update all docs and examples to use new connect() API
Copilot AI review requested due to automatic review settings May 12, 2026 22:41
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR streamlines MCP server setup by introducing a one-liner CapiscioMCPServer.connect() factory and updates docs/examples accordingly, while also adding a short-lived guard decision cache and improving badge renewal compatibility with domain-scoped issuance.

Changes:

  • Add CapiscioMCPServer.connect() factory plus related credential/telemetry conveniences.
  • Add a 5-second decision cache in @guard to reduce repeated gRPC calls.
  • Extend badge renewal to support a domain parameter and broaden renewal response parsing (badge/token).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
README.md Updates examples to use CapiscioMCPServer.connect() and documents the new entrypoint.
docs/guides/deployment.md Updates deployment snippets and env var docs to reference CapiscioMCPServer.connect().
capiscio_mcp/keeper.py Adds optional domain to renewal requests and accepts token in renewal responses.
capiscio_mcp/integrations/mcp.py Adds CapiscioMCPServer.connect() factory and flushes pending telemetry on shutdown.
capiscio_mcp/guard.py Introduces a global decision cache and adjusts some log levels.
capiscio_mcp/connect.py Passes a derived domain into ServerBadgeKeeper and adds sync convenience methods.
capiscio_mcp/init.py Suppresses gRPC C-core verbosity at import time; updates top-level package docstring examples.
Comments suppressed due to low confidence (1)

docs/guides/deployment.md:131

  • The Lambda snippet uses async def handler(...) but calls CapiscioMCPServer.connect() (sync). With the current implementation relying on asyncio.run(), this is likely to raise when the runtime already has an event loop. Consider updating the snippet to use an async identity setup path (or provide a truly async CapiscioMCPServer.connect).
```python
import json
from capiscio_mcp.integrations.mcp import CapiscioMCPServer

async def handler(event, context):
    # Key injected via Lambda environment variables or Secrets Manager
    server = CapiscioMCPServer.connect()

Comment thread capiscio_mcp/guard.py
Comment thread capiscio_mcp/guard.py
Comment thread capiscio_mcp/connect.py
Comment thread capiscio_mcp/connect.py
Comment thread capiscio_mcp/integrations/mcp.py
Comment thread docs/guides/deployment.md
Comment thread capiscio_mcp/guard.py
Comment thread capiscio_mcp/integrations/mcp.py
beonde added 2 commits May 12, 2026 19:56
- Fix guard test failures: clear decision cache between tests (autouse fixture)
- Fix api_key bug: use effective_api_key for BadgeKeeper construction
- Improve decision cache: add max size bound (256), thread safety via lock
- Fix deployment.md: remove async from sync connect() examples
- Test cache put/get, miss, expiry eviction, key independence
- Covers _cache_get, _cache_put, and _decision_cache internals
Copilot AI review requested due to automatic review settings May 12, 2026 23:59
@github-actions
Copy link
Copy Markdown

✅ Integration tests passed! capiscio-core gRPC tests working.

- Test eviction of oldest entries when cache hits max size
- Test expired entry sweep before oldest-entry eviction
- Covers guard.py lines 150-158 (eviction logic)
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (3)

capiscio_mcp/guard.py:360

  • The decision cache ignores inputs that can change the result for the same (badge, tool) such as effective_config (min_trust_level, trusted_issuers, allowed_tools, policy_version, require_badge), capability_class, and deny_on_unknown_class. Because the cache is module-global, running multiple servers/tools in the same process (or calling evaluate_tool_access() with different configs) can incorrectly reuse a prior decision and weaken enforcement. Consider including the relevant config/capability fields (or a stable hash of them) in the cache key, or scoping the cache per-server instance instead of globally.
    # Check decision cache — same badge + same tool = same decision
    cache_key_jws = effective_credential.badge_jws or ""
    cached = _cache_get(cache_key_jws, tool_name)
    if cached is not None:
        logger.debug("Decision cache hit: tool=%s decision=%s", tool_name, cached.decision.value)
        return cached

capiscio_mcp/guard.py:462

  • Caching and returning a full GuardResult reuses evidence_id/evidence_json across multiple tool invocations, and also skips the core RPC that appears to generate per-call evidence. This breaks the stated “every guarded tool call produces a cryptographic evidence record” behavior and can lead to duplicate evidence IDs being emitted/logged. If caching is required, avoid caching evidence-bearing results (cache only the policy decision inputs/outputs and still generate fresh evidence per call), or move caching into capiscio-core where evidence can still be minted per invocation.
    result = GuardResult(
        decision=decision,
        deny_reason=deny_reason,
        deny_detail=response.deny_detail or None,
        agent_did=response.agent_did or None,
        badge_jti=response.badge_jti or None,
        auth_level=auth_level,
        trust_level=response.trust_level,
        evidence_id=response.evidence_id,
        evidence_json=response.evidence_json,
        error_code=response.error_code or None,
        requested_capability=response.requested_capability or None,
        presented_capability=response.presented_capability or None,
    )

    # Cache for subsequent calls with the same badge + tool
    _cache_put(cache_key_jws, tool_name, result)

    return result

docs/guides/deployment.md:118

  • The Docker “Server Code” example defines main() but never calls it (no main() invocation / if __name__ == "__main__": ...). As written, python server.py would do nothing, which makes the deployment snippet misleading. Either remove the main() wrapper and run at module scope, or add an explicit call to main()/server.run() in the example.
```python
from capiscio_mcp.integrations.mcp import CapiscioMCPServer

def main():
    # Reads CAPISCIO_SERVER_ID, CAPISCIO_API_KEY, and
    # CAPISCIO_SERVER_PRIVATE_KEY_PEM from environment
    server = CapiscioMCPServer.connect()

    @server.tool(min_trust_level=2)
    async def my_tool(param: str) -> str:
        return f"Result: {param}"

    server.run()
</details>

Comment thread capiscio_mcp/guard.py
Comment thread tests/conftest.py
Comment thread capiscio_mcp/integrations/mcp.py
Comment thread capiscio_mcp/connect.py
@github-actions
Copy link
Copy Markdown

✅ Integration tests passed! capiscio-core gRPC tests working.

The Python guard was making an HTTP POST to the cloud PDP endpoint
on every ALLOW decision that missed the 5s cache, adding 50-60ms
latency. This is unnecessary because the Go sidecar already evaluates
OPA policy locally via BundleManager when CAPISCIO_BUNDLE_URL is set
(which connect() already configures).

Removed:
- _evaluate_org_policy() and the second-phase PDP block in guard.py
- _pip_config singleton, set_pip_config(), get_pip_config()
- pdp_endpoint parameter from connect()
- Step 7 (auto-configure PDP) from connect()
- Corresponding exports from __init__.py

PIPConfig and PolicyClient remain as public API for advanced direct use.

Resolves: enforcement-demo scenarios 2/3 latency (50-60ms → sub-10ms)
@beonde beonde closed this May 13, 2026
@beonde beonde reopened this May 13, 2026
@beonde
Copy link
Copy Markdown
Member Author

beonde commented May 13, 2026

Reopening to retrigger CI

@beonde beonde closed this May 13, 2026
@beonde beonde reopened this May 13, 2026
Copilot AI review requested due to automatic review settings May 13, 2026 14:12
The PDP auto-configuration block from main referenced pdp_endpoint
but it was missing from the function signature after the merge,
causing NameError at runtime.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 11 comments.

Comments suppressed due to low confidence (2)

capiscio_mcp/guard.py:329

  • The cache key collapses every non-badge caller to the empty string, so an API-key decision and an anonymous decision for the same tool can be reused interchangeably. For example, an API-key ALLOW can let a later anonymous caller bypass the core check, or an anonymous DENY can incorrectly block an API-key caller; include the credential type/API key (or avoid caching non-badge credentials).
    # Check decision cache — same badge + same tool = same decision
    cache_key_jws = effective_credential.badge_jws or ""
    cached = _cache_get(cache_key_jws, tool_name)

capiscio_mcp/integrations/mcp.py:418

  • This new public factory has no test coverage. A small test that patches MCPServerIdentity.from_env_sync and asserts the resulting server is constructed with that identity would catch regressions in the one-liner API without hitting the registry.
    @classmethod
    def connect(cls, **kwargs: Any) -> "CapiscioMCPServer":
        """Connect to the CapiscIO registry and return a ready-to-use server.

        Combines ``MCPServerIdentity.from_env_sync()`` and server construction
        into a single call — the MCP server equivalent of ``CapiscIO.connect()``
        from the agent SDK.

        Any extra keyword arguments are forwarded to the constructor
        (e.g. ``default_min_trust_level``, ``name``).

        Example::

            server = CapiscioMCPServer.connect()

            @server.tool(min_trust_level=1)
            async def place_order(sku: str, quantity: int) -> str:
                ...

            server.run()
        """
        from capiscio_mcp.connect import MCPServerIdentity

        identity = MCPServerIdentity.from_env_sync()
        return cls(identity=identity, **kwargs)

Comment thread capiscio_mcp/guard.py
Comment thread capiscio_mcp/guard.py
Comment thread capiscio_mcp/connect.py Outdated
Comment thread capiscio_mcp/__init__.py
Comment thread capiscio_mcp/guard.py
Comment thread capiscio_mcp/connect.py
Comment thread capiscio_mcp/keeper.py
Comment thread capiscio_mcp/integrations/mcp.py
Comment thread README.md
Comment thread capiscio_mcp/integrations/mcp.py
The branch's guard.py delegates policy evaluation to Go core via
gRPC (EvaluateToolAccess), not via the removed set_pip_config/PIPConfig
module-level singleton. The PDP block was brought in during merge
resolution from main but is incompatible with the branch's architecture.

Go core reads CAPISCIO_PDP_ENDPOINT directly from the environment.
Copilot AI review requested due to automatic review settings May 13, 2026 14:25
@github-actions
Copy link
Copy Markdown

✅ Integration tests passed! capiscio-core gRPC tests working.

@beonde beonde merged commit 9ab139f into main May 13, 2026
14 checks passed
@beonde beonde deleted the feat/connect-one-liner branch May 13, 2026 14:31
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 12 comments.

Comment thread capiscio_mcp/guard.py
Comment on lines +327 to +328
# Check decision cache — same badge + same tool = same decision
cache_key_jws = effective_credential.badge_jws or ""
Comment thread capiscio_mcp/guard.py
Comment on lines +327 to +332
# Check decision cache — same badge + same tool = same decision
cache_key_jws = effective_credential.badge_jws or ""
cached = _cache_get(cache_key_jws, tool_name)
if cached is not None:
logger.debug("Decision cache hit: tool=%s decision=%s", tool_name, cached.decision.value)
return cached
from capiscio_mcp.connect import MCPServerIdentity

identity = MCPServerIdentity.from_env_sync()
return cls(identity=identity, **kwargs)
Comment thread capiscio_mcp/keeper.py
"X-Capiscio-Registry-Key": self.api_key,
"Content-Type": "application/json",
}
body: dict = {}
Comment thread capiscio_mcp/keeper.py
Comment on lines +228 to +230
body: dict = {}
if self.domain:
body["domain"] = self.domain
Comment thread capiscio_mcp/__init__.py
Comment on lines 88 to 92
guard_sync,
GuardConfig,
GuardResult,
compute_params_hash,
evaluate_tool_access,
Comment thread capiscio_mcp/connect.py
Comment on lines +655 to +659
@classmethod
def connect_sync(
cls,
server_id: str,
api_key: Optional[str] = None,
Comment thread capiscio_mcp/guard.py
Comment on lines +327 to +328
# Check decision cache — same badge + same tool = same decision
cache_key_jws = effective_credential.badge_jws or ""
Comment thread capiscio_mcp/__init__.py
Comment on lines +59 to +63
import os as _os

# Suppress gRPC C-core stderr noise (ev_poll_posix.cc, fork_posix.cc, etc.)
# before any gRPC import. Library users should not see low-level C-core logs.
_os.environ.setdefault("GRPC_VERBOSITY", "NONE")
Comment on lines +394 to +398
@classmethod
def connect(cls, **kwargs: Any) -> "CapiscioMCPServer":
"""Connect to the CapiscIO registry and return a ready-to-use server.

Combines ``MCPServerIdentity.from_env_sync()`` and server construction
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants