Metadata-Version: 2.4
Name: kurd
Version: 0.7.0
Classifier: Development Status :: 5 - Production/Stable
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Rust
Classifier: Topic :: Software Development :: Libraries
License-File: LICENSE
Summary: Enterprise Model Context Protocol gateway with multi-tenancy, billing, RBAC, DLQ, idempotency, and secrets management.
Keywords: mcp,model-context-protocol,gateway,rust,python,ai
Author-email: Semko Kermashani <sn391@yahoo.com>
License: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Homepage, https://github.com/sn391/kurd
Project-URL: Issues, https://github.com/sn391/kurd/issues
Project-URL: Repository, https://github.com/sn391/kurd

# Kurd MCP

[![PyPI](https://img.shields.io/pypi/v/kurd)](https://pypi.org/project/kurd/)
[![Python](https://img.shields.io/pypi/pyversions/kurd)](https://pypi.org/project/kurd/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![CI](https://img.shields.io/github/actions/workflow/status/sn391/kurd/CI.yml?branch=main)](https://github.com/sn391/kurd/actions)

**Kurd** is a high-performance [Model Context Protocol](https://modelcontextprotocol.io) (MCP) gateway for Python, powered by Rust.

The Rust data plane handles HTTP serving, JSON-RPC dispatch, tool routing, upstream aggregation, caching, retries, circuit breaking, backpressure, rate limiting, and Prometheus metrics. The Python layer provides the developer API — tool registration, runtime configuration, and an optional enterprise feature set.

> Targets MCP protocol revision **2026-07-28**. Fully typed (PEP 561).

---

## Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [CLI](#cli)
- [Registering Tools](#registering-tools)
- [Mounting Upstream Servers](#mounting-upstream-servers)
- [Tool Discovery Filtering](#tool-discovery-filtering)
- [Admin API](#admin-api)
- [Runtime Configuration](#runtime-configuration)
- [Security](#security)
- [Multi-tenancy & Policy Engine](#multi-tenancy--policy-engine)
- [Observability](#observability)
- [MCP Protocol Compliance](#mcp-protocol-compliance)
- [Enterprise Features](#enterprise-features)
- [Performance](#performance)
- [Architecture](#architecture)
- [Development](#development)
- [Project Structure](#project-structure)
- [License](#license)

---

## Installation

```bash
pip install kurd
```

Requires Python 3.10+ and a 64-bit platform. Pre-built wheels are available for Windows, Linux (x86-64, aarch64), and macOS (x86-64, Apple Silicon).

---

## Quick Start

```python
from kurd import Router
from kurd._kurd import start_http_gateway

router = Router()

@router.tool()
async def add(a: int, b: int) -> int:
    """Add two integers."""
    return a + b

# Blocks until stop_http_gateway() is called or the process exits.
start_http_gateway("0.0.0.0:9200")
```

The gateway starts the following endpoints:

| Path | Method | Purpose |
|------|--------|---------|
| `/mcp` | `POST` | JSON-RPC 2.0 MCP endpoint |
| `/health` | `GET` | Liveness probe — returns `200 OK` |
| `/status` | `GET` | Runtime, cache, upstream, and circuit-breaker snapshot |
| `/metrics` | `GET` | Prometheus metrics |
| `/admin/servers` | `GET` | List registered upstream servers |
| `/admin/servers` | `POST` | Add or replace an upstream server |
| `/admin/servers/{name}` | `DELETE` | Remove an upstream server |
| `/admin/tools` | `GET` | List all tools (local + upstream) with source label |
| `/admin/tools/reload` | `POST` | Expire the tool-list cache immediately |
| `/admin/tools/namespaces` | `GET` | List upstream namespaces |

Call the gateway:

```bash
curl -s http://localhost:9200/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"add","arguments":{"a":3,"b":4}}}'
```

```json
{"jsonrpc":"2.0","id":1,"result":{"resultType":"complete","content":[{"type":"text","text":"7"}],"isError":false}}
```

---

## CLI

Kurd ships a `kurd` command installed alongside the package.

```
Usage: kurd <COMMAND>

Commands:
  serve   Start the HTTP MCP gateway

Options:
  -h, --help  Show this message and exit
```

### `kurd serve`

```bash
kurd serve [--host HOST] [--port PORT] [--token TOKEN]
```

| Flag | Default | Description |
|------|---------|-------------|
| `--host` | `0.0.0.0` | Bind address |
| `--port` | `8000` | Bind port |
| `--token` | — | Bearer token for authentication (overrides `KURD_AUTH_TOKEN`) |

```bash
# Start on port 8000 with no authentication
kurd serve

# Start on a specific address with a bearer token
kurd serve --host 127.0.0.1 --port 9200 --token my-secret

# Use an environment variable for the token
KURD_AUTH_TOKEN=my-secret kurd serve --port 9200
```

---

## Registering Tools

### Decorator API

```python
from kurd import Router

router = Router()

@router.tool()
async def search(query: str, limit: int = 10) -> list[str]:
    """Search the knowledge base."""
    return [f"result {i}" for i in range(limit)]
```

Type annotations are converted to a JSON Schema `inputSchema` automatically:

| Python type | JSON Schema type |
|-------------|-----------------|
| `int` | `integer` |
| `float` | `number` |
| `bool` | `boolean` |
| `str` | `string` |
| `list[T]` | `array` with item schema |
| `dict` | `object` |
| `Optional[T]` / `T \| None` | schema of `T` |

Parameters with defaults become optional; parameters without defaults are added to `required`.

### Hot-reloading

Replace a tool's implementation at runtime without restarting the gateway:

```python
router.reload_tool("search", new_search_function)
```

### Unregistering

```python
router.unregister_tool("search")
```

### Introspection

```python
router.list_tools()      # -> ["add", "search", ...]
router.list_upstreams()  # -> [("github", "http://..."), ...]
```

---

## Mounting Upstream Servers

Kurd aggregates remote MCP servers alongside local tools.

```python
router.mount("github", "http://github-mcp.internal:9300")
router.mount("jira",   "http://jira-mcp.internal:9300")
```

Upstream tools are prefixed with the upstream name:

```text
github.create_issue
jira.create_ticket
```

Clients discover all tools — local and upstream — through a single `tools/list` call. Kurd fetches remote tool lists concurrently, caches them with a configurable TTL, and follows pagination automatically.

### Unmounting and cache invalidation

```python
router.unmount("github")   # stop routing to this upstream
router.refresh_tools()     # expire the tool list cache immediately
```

### Upstream behaviour

- **Connection pool**: persistent HTTP/1.1 connections via Reqwest
- **Retry**: up to 3 attempts with exponential backoff + jitter
- **Circuit breaker**: opens after 5 consecutive failures; resets after 30 s
- **Timeout**: configurable per `RuntimeConfig.upstream_timeout_ms`
- **Private-network policy**: loopback/private URLs blocked by default unless `set_allow_private_upstreams(True)` is called

---

## Tool Discovery Filtering

Clients can scope a `tools/list` call with an optional `filter` parameter — without any server-side configuration needed.

### Namespace filter

Returns only tools belonging to a specific upstream:

```json
{
  "jsonrpc": "2.0", "id": 1, "method": "tools/list",
  "params": { "filter": { "namespace": "github" } }
}
```

### Search filter

Case-insensitive substring match across tool name **and** description:

```json
{
  "jsonrpc": "2.0", "id": 1, "method": "tools/list",
  "params": { "filter": { "search": "file" } }
}
```

### Discovery metadata

Every `tools/list` response includes a `_kurd` object:

```json
{
  "result": {
    "tools": [...],
    "_kurd": { "available": 12, "returned": 3 }
  }
}
```

`available` is the count after tenant restrictions; `returned` is the count after the client filter. An LLM agent can use these counts to know whether to refine its query.

> **Security**: client filters always run _after_ per-tenant restrictions. A tenant cannot use `search` or `namespace` to enumerate tools outside their allowlist.

---

## Admin API

The Admin API lets operators manage the gateway at runtime without a restart. All admin endpoints accept an optional `Authorization: Bearer <token>` header.

### Set a dedicated admin token

```python
router.set_admin_token("admin-secret")
# router.clear_admin_token()  # fall back to MCP bearer token / open
```

Or via the module-level API:

```python
from kurd import set_admin_token, clear_admin_token
set_admin_token("admin-secret")
```

### Manage upstream servers

```bash
# List
curl http://localhost:9200/admin/servers

# Add / replace
curl -X POST http://localhost:9200/admin/servers \
  -H 'Content-Type: application/json' \
  -d '{"name": "github", "url": "http://github-mcp.internal:9300/mcp"}'

# Remove
curl -X DELETE http://localhost:9200/admin/servers/github
```

Response codes: `201 Created` (new), `200 OK` (replaced), `404 Not Found` (delete miss), `400 Bad Request` (invalid URL or empty name).

### Inspect tools

```bash
# All tools with source label
curl http://localhost:9200/admin/tools

# Reload (expire cache)
curl -X POST http://localhost:9200/admin/tools/reload

# List upstream namespaces
curl http://localhost:9200/admin/tools/namespaces
```

---

## Runtime Configuration

All gateway tunables are collected in `RuntimeConfig`:

```python
from kurd import Router, RuntimeConfig

router = Router()
router.configure_runtime(RuntimeConfig(
    # Concurrency
    global_concurrency   = 512,   # max simultaneous in-flight requests
    upstream_concurrency = 64,    # max simultaneous upstream calls
    python_concurrency   = 64,    # max simultaneous Python tool calls
    upstream_timeout_ms  = 30_000,

    # Logging
    request_logging      = False, # structured per-request log lines

    # Rate limiting
    rate_limiting_enabled   = True,
    rate_limit_per_ip_rps   = 1_000,
    rate_limit_global_rps   = 10_000,

    # IP allowlist (None = allow all)
    ip_allowlist         = ["192.168.1.0/24", "10.0.0.1"],

    # Tool cache
    tools_cache_ttl_ms   = 30_000,

    # Enterprise (all off by default)
    enable_dlq                  = False,
    enable_idempotency          = False,
    secrets_backend             = "env",
    enable_webhooks             = False,
    enable_distributed_state    = False,
    distributed_state_backend   = "memory",
    redis_url                   = "redis://localhost:6379/0",
    enable_distributed_tracing  = False,
))
```

`configure_runtime` also accepts keyword arguments directly for ergonomic one-liners:

```python
router.configure_runtime(request_logging=True, rate_limiting_enabled=True)
```

### Runtime status

```python
status = router.runtime_status()
# {
#   "global_active": 3,
#   "global_limit": 512,
#   "python_active": 1,
#   "upstream_metrics": {...},
#   "cache": {"hits": 142, "misses": 3},
#   ...
# }
```

---

## Security

### Bearer token authentication

Set a bearer token before starting the gateway. Requests missing or carrying a wrong token receive `401 Unauthorized`.

```python
from kurd._kurd import set_http_bearer_token, clear_http_bearer_token

set_http_bearer_token("my-production-token")
# clear_http_bearer_token()  # disable authentication
```

Via environment variable (loaded automatically at gateway start):

```bash
KURD_AUTH_TOKEN=my-production-token kurd serve
```

Tokens are compared with a constant-time byte comparison to prevent timing attacks.

### IP allowlist

```python
from kurd import set_ip_allowlist, clear_ip_allowlist

set_ip_allowlist(["10.0.0.1", "10.0.0.2"])
clear_ip_allowlist()  # allow all IPs again
```

Or through `RuntimeConfig.ip_allowlist`. Blocked IPs receive `403 Forbidden`.

### Rate limiting

```python
router.configure_runtime(
    rate_limiting_enabled=True,
    rate_limit_per_ip_rps=1_000,
    rate_limit_global_rps=10_000,
)
```

Rate-limited requests receive `429 Too Many Requests` with a `Retry-After: 1` header and a `retryAfterMs` field in the JSON-RPC error body.

### Additional safeguards

| Safeguard | Details |
|-----------|---------|
| Request size cap | 1 MiB hard limit; `413` on excess |
| Content-type validation | Must be `application/json`; `-32600` otherwise |
| Upstream URL validation | Rejects credentials, fragments, and unsupported schemes |
| Private-network policy | Upstream calls to loopback/RFC1918 blocked by default |
| CORS | `OPTIONS /mcp` returns correct preflight headers; `POST` responses include `Access-Control-Allow-Origin: *` |
| Overload rejection | `503` when global concurrency limit is reached |

For internet-facing deployments, terminate TLS at a reverse proxy (nginx, Caddy, AWS ALB) and apply network-level controls there.

---

## Multi-tenancy & Policy Engine

Kurd ships a built-in `TenantManager` that wires directly into the Rust request hot path. One call to `set_policy_engine()` activates both `tools/call` gating and `tools/list` filtering simultaneously.

### Basic setup

```python
from kurd import Router, TenantManager

manager = TenantManager()

# Add tenants with explicit tool allowlists
manager.add_tenant("acme",   name="Acme Corp",  allowed_tools=["add", "search"], api_key="sk-acme")
manager.add_tenant("devops", name="DevOps Team", allowed_tools=["*"],            api_key="sk-ops")

router = Router()
router.set_policy_engine(manager)
# router.clear_policy_engine()  # disable, all requests allowed again
```

### What it enforces

| Behaviour | Details |
|-----------|---------|
| `tools/call` gating | Unknown API key or tool outside allowlist → `403 Forbidden` (JSON-RPC `-32004`) |
| `tools/list` filtering | Response contains only the tools the caller may invoke |
| Wildcard support | `"*"` in `allowed_tools` passes all tools through |
| Namespace wildcard | `"github.*"` passes all tools prefixed `github.` |
| Unknown key | Returns an empty `tools` list and `403` on any `tools/call` |

### Calling with a tenant key

```bash
curl http://localhost:9200/mcp \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer sk-acme' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# → returns only ["add", "search"]
```

### Prometheus metric

```
kurd_policy_denied_total   # counter — requests blocked by the policy engine
```

### Per-tenant quotas and billing

For rate-per-tenant quotas, request tracking, and billing see the [Enterprise Features](#enterprise-features) section.

---

## Observability

### Structured logging

Enable per-request log lines (goes to stdout in the format chosen by `KURD_LOG`):

```python
router.configure_runtime(request_logging=True)
```

Control log verbosity via environment variable:

```bash
KURD_LOG=kurd=debug kurd serve   # debug, info, warn, error
RUST_LOG=info kurd serve         # fallback if KURD_LOG is unset
```

Log level can also be changed at runtime via the `logging/setLevel` MCP method.

### Prometheus metrics

```bash
curl http://localhost:9200/metrics
```

| Metric | Type | Description |
|--------|------|-------------|
| `kurd_requests_total{status}` | counter | Total requests by status (`total`, `completed`, `rejected`) |
| `kurd_policy_denied_total` | counter | Requests blocked by the policy engine (`-32004`) |
| `kurd_requests_active` | gauge | In-flight requests right now |
| `kurd_requests_peak_active` | gauge | Highest concurrent request count since startup |
| `kurd_request_latency_ms` | gauge | Rolling average latency (ms) |
| `kurd_request_latency_histogram_ms_bucket{le}` | histogram | Latency distribution (1ms … 5000ms + Inf) |
| `kurd_request_latency_histogram_ms_count` | counter | Total completed requests counted in histogram |
| `kurd_request_latency_histogram_ms_sum` | counter | Total latency (ms) summed across all requests |
| `kurd_python_active_calls` | gauge | Active Python tool invocations |
| `kurd_python_peak_active_calls` | gauge | Peak simultaneous Python tool invocations |
| `kurd_python_rejections_total` | counter | Python tool calls dropped due to concurrency limit |
| `kurd_upstream_requests_total{upstream}` | counter | Requests forwarded per upstream |
| `kurd_upstream_successes_total{upstream}` | counter | Successful upstream calls |
| `kurd_upstream_failures_total{upstream}` | counter | Failed upstream calls |
| `kurd_upstream_retries_total{upstream}` | counter | Retry attempts per upstream |
| `kurd_upstream_latency_ms{upstream}` | gauge | Average upstream round-trip latency |
| `kurd_upstream_circuit_breaker_state{upstream}` | gauge | `0` = closed, `1` = open |
| `kurd_upstream_active_calls{upstream}` | gauge | Current in-flight calls per upstream |
| `kurd_upstream_peak_active_calls{upstream}` | gauge | Peak in-flight calls per upstream |
| `kurd_upstream_rejections_total` | counter | Upstream calls dropped due to concurrency limit |
| `kurd_cache_hits_total` | counter | Tool-list cache hits |
| `kurd_cache_misses_total` | counter | Tool-list cache misses |
| `kurd_cache_invalidations_total` | counter | Cache invalidations (manual or TTL expiry) |
| `kurd_concurrency_limit{type}` | gauge | Configured limits: `global`, `upstream`, `python` |

#### Prometheus scrape config

```yaml
# prometheus.yml
scrape_configs:
  - job_name: kurd
    static_configs:
      - targets: ["localhost:9200"]
    metrics_path: /metrics
    scrape_interval: 15s
```

#### Datadog

```yaml
# datadog.yaml
instances:
  - openmetrics_endpoint: http://localhost:9200/metrics
    namespace: kurd
    metrics: ["kurd_.*"]
```

### OpenTelemetry

Kurd exports real OTLP spans from the Rust core — no Python OpenTelemetry SDK required.

#### Quick setup

```python
from kurd import Router
from kurd.telemetry import setup_otel

router = Router()

# Activates Rust-side OTLP export. setup_otel returns an OTELTracer for
# any additional Python-side instrumentation you want.
setup_otel(
    service_name = "my-gateway",
    endpoint     = "http://otel-collector:4318",  # OTLP HTTP receiver
)
```

Or directly via the Router:

```python
router.configure_otel("http://otel-collector:4318", service_name="my-gateway")
# router.clear_otel()  # disable export
```

#### What gets traced

- Every request that passes authentication, rate limiting, and concurrency checks produces one server-side span.
- Spans are exported **fire-and-forget** (2-second timeout, errors silently dropped) so a slow or unavailable collector never adds latency.
- The OTLP JSON payload is sent to `{endpoint}/v1/traces`.

#### W3C `traceparent` propagation

Every MCP response carries a `traceparent` header so downstream services and LLM agents can continue the trace:

```
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
```

If the incoming request already carries a `traceparent`, Kurd preserves the `trace_id` and issues a new `span_id`. Malformed headers start a fresh trace.

`OTELConfig.service_version` defaults to the installed `kurd` package version automatically.

### Health checks

```python
from kurd.health_checks import HealthCheckManager

hc = HealthCheckManager()

# Register a custom check
async def check_db():
    ...

hc.register_check("database", check_db, critical=True)

# Kubernetes probes
readiness = await hc.check_readiness()  # all critical checks pass
liveness  = await hc.check_liveness()   # process is running and active
```

---

## MCP Protocol Compliance

Kurd implements the MCP **2026-07-28** protocol revision.

### Supported methods

| Method | Behaviour |
|--------|-----------|
| `initialize` | Returns `protocolVersion`, `capabilities`, and `serverInfo` |
| `ping` | Returns `{}` |
| `server/discover` | Returns capabilities, supported versions, and server identity |
| `tools/list` | Aggregates local + upstream tools; cursor-based pagination; `params.filter.namespace` / `params.filter.search`; `_kurd` metadata; per-tenant filtering when policy engine is active |
| `tools/call` | Routes to local Python tool or upstream server; policy engine gate when active |
| `resources/list` | Returns empty list with `ttlMs` and `cacheScope` |
| `resources/read` | Returns `{"contents": []}` |
| `prompts/list` | Returns empty list with `ttlMs` and `cacheScope` |
| `prompts/get` | Returns `-32602` (gateway holds no prompts) |
| `completion/complete` | Returns `{"values": [], "hasMore": false}` |
| `logging/setLevel` | Applies log level to the tracing filter at runtime |
| `notifications/*` | Accepted silently — returns `202 Accepted` with empty body |

### Modern HTTP headers

When a client sends `Mcp-Protocol-Version: 2026-07-28`, Kurd additionally validates:

- `Mcp-Method` header matches the JSON-RPC `method` field
- `Mcp-Name` header matches `params.name` for `tools/call`

Mismatched headers return `-32020`. Unsupported protocol versions return `-32019`.

---

## Enterprise Features

Enable features through `RuntimeConfig` or by importing the relevant manager class directly.

### Multi-tenancy

See [Multi-tenancy & Policy Engine](#multi-tenancy--policy-engine) above for the full policy engine and tool-filtering setup.

```python
from kurd import TenantManager

manager = TenantManager()
manager.add_tenant(
    tenant_id="acme",
    name="Acme Corp",
    quota_rps=100,
    allowed_tools=["add", "search"],
    api_key="sk-acme",
)
```

Each tenant receives a unique API key. Quotas and tool ACLs are enforced independently.

### Billing

```python
from kurd.billing import BillingManager

billing = BillingManager()
billing.set_pricing({"add": {"per_call": 0.001, "per_latency_ms": 0.0001}})
billing.track_call(tenant_id="acme", tool_name="add", latency_ms=12.5, success=True)

report = billing.get_usage_report("acme", period="2026-08")
```

Supported models: per-request, per-latency, tiered, hybrid.

### Request idempotency

```python
router.configure_runtime(enable_idempotency=True)
mgr = router.get_idempotency()

is_dup, cached = mgr.check_idempotent_key("req-abc-123", tenant_id="acme")
if is_dup:
    return cached

result = run_tool()
mgr.store_result("req-abc-123", "acme", result)
```

Backed by SQLite with a 24-hour TTL.

### Dead-letter queue

```python
router.configure_runtime(enable_dlq=True, dlq_storage_path="/data/kurd/dlq")
dlq = router.get_dlq()

dlq.add_message(request_id="req-123", tenant_id="acme",
                tool_name="add", arguments={"a":1,"b":2}, error="timeout")

dlq.register_replay_handler("add", handler)
success, error = dlq.replay_message("dlq_abc123")

stats = dlq.get_statistics(tenant_id="acme")
dlq.cleanup_archived(days=30)
```

Replay uses exponential backoff up to 1 hour.

### Secrets management

```python
from kurd.secrets_management import SecretsManager

# Kubernetes in-cluster | HashiCorp Vault | AWS Secrets Manager | env (default)
manager = SecretsManager(backend="vault",
                          vault_addr="https://vault.example.com",
                          vault_token="s.xxxxx")

secret = manager.get_secret("db_password")
```

Third-party dependencies (`kubernetes`, `hvac`, `boto3`) are imported lazily — only when the matching backend is activated.

### Webhooks

```python
router.configure_runtime(enable_webhooks=True)
hooks = router.get_webhooks()

hooks.register_webhook(
    url="https://example.com/hooks",
    events=["error", "rate_limit_exceeded"],
    tenant_id="acme",
)
```

Deliveries are HMAC-SHA256 signed and logged for audit via `get_deliveries()`.

### Distributed state

```python
router.configure_runtime(
    enable_distributed_state=True,
    distributed_state_backend="redis",
    redis_url="redis://localhost:6379/0",
)
state = router.get_distributed_state()
state.set("gateway:version", 42)
state.increment("counters:acme:calls")
```

Use `backend="memory"` for single-instance deployments.

### Distributed tracing

```python
from kurd.distributed_tracing import extract_context

trace = extract_context(incoming_headers)
span  = trace.create_span("tool_execution", {"tool": "add"})
span.set_attribute("result", 42)
span.end()
```

Follows W3C Trace Context. Tracing state is available in `router.runtime_status()` when enabled.

---

## Performance

Benchmarks from a Windows development machine (Python 3.12, release build):

| Scenario | Concurrency | Throughput | p50 | p95 | p99 | Errors |
|---|---:|---:|---:|---:|---:|---:|
| Local Python tool | 10 | 594.5 req/s | 14.9 ms | 23.9 ms | 28.7 ms | 0% |
| Local Python tool | 50 | 587.9 req/s | 33.3 ms | 87.8 ms | 119.1 ms | 0% |
| Local Python tool | 100 | 556.0 req/s | 18.3 ms | 29.5 ms | 32.4 ms | 0% |
| Upstream tool | 10 | 412.2 req/s | 21.8 ms | 36.4 ms | 42.7 ms | 0% |
| Upstream tool | 50 | 229.8 req/s | 20.6 ms | 534.6 ms | 549.3 ms | 0% |
| Sustained burst | 100 | 573.3 req/s | 73.5 ms | 179.1 ms | 218.5 ms | 0% |

Results depend on hardware, OS, Python version, and network conditions.

```bash
python -m pytest tests/test_load.py -q -s
```

---

## Architecture

```
Python application
       │
       ▼
  kurd.Router                      ← Python API layer
       │
       ├── Policy engine (set_policy_engine)
       │   TenantManager callbacks wired into Rust hot path
       │
       ├── Enterprise modules (optional, lazy)
       │   multitenancy · billing · idempotency · DLQ
       │   secrets · webhooks · distributed state
       │
       ▼
   PyO3 boundary
       │
       ▼
  Rust MCP gateway (Axum + Tokio)
       │
       ├── HTTP handler  ─────────────────────────────────┐
       │   content-type · auth · IP allowlist              │
       │   rate limiting · concurrency backpressure        │
       │   CORS · request ID · W3C traceparent             │
       │   OTLP span export (fire-and-forget)              │
       │                                                   │
       ├── Admin API (/admin/*)                            │
       │   server CRUD · tool listing · cache reload       │
       │                                                   │
       ├── MCP dispatcher                                  │
       │   initialize · ping · server/discover             │
       │   tools/list (paginated, filtered, _kurd meta)    │
       │   tools/call (policy gate) · resources · prompts  │
       │   completion · logging · notifications (202)      │
       │                                                   │
       ├── Local Python tools ◄── PyO3 callback            │
       │   (Rayon-parallel batch parsing)                  │
       │                                                   │
       └── Upstream MCP servers                           │
           retry · circuit breaker · cache · metrics      ◄┘
```

The Rust layer holds all mutable gateway state in lock-free atomics and `RwLock`-guarded maps. Python code never touches the hot path after registration.

---

## Development

### Prerequisites

- Rust stable toolchain (`rustup update stable`)
- Python 3.10+
- `maturin` and `pytest`

```bash
pip install maturin pytest
```

### Build

```bash
# Development build (fast iteration)
maturin develop

# Optimised build (benchmarks, pre-release testing)
maturin develop --release

# Release wheel
maturin build --release
```

### Test

```bash
python -m pytest -q
```

The test suite covers:

- JSON-RPC parsing and fast batch parsing (Rayon)
- Local sync and async tools
- `initialize` handshake and lifecycle methods
- `tools/list` pagination
- `completion/complete`, `notifications/202`, CORS preflight
- Upstream discovery, routing, and concurrency
- Circuit breaker, retry, and timeout behaviour
- Tool-list cache hits, misses, and invalidation
- Bearer authentication (accepted and rejected)
- IP allowlist enforcement
- Rate-limit rejection and `Retry-After` header
- Request-size and content-type guards
- Prometheus metrics output
- Load and burst behaviour
- **Policy engine**: allow, deny, unknown key, clear (P0)
- **Admin API**: server CRUD, tool listing, reload, auth token (P1)
- **Per-tenant tool filtering**: wildcard, restricted, unknown key, clear (P2)
- **Client tool discovery**: namespace filter, search, combined, `_kurd` metadata, bypass prevention (P3)
- **OpenTelemetry**: `traceparent` presence/format, trace-id propagation, span-id rotation, malformed input, enable/disable (P4)

### Linting

```bash
cargo check
cargo clippy -- -D warnings
```

### Environment variables

| Variable | Purpose |
|----------|---------|
| `KURD_AUTH_TOKEN` | Bearer token loaded automatically at gateway start |
| `KURD_LOG` | Tracing filter (e.g. `kurd=debug`). Takes precedence over `RUST_LOG` |
| `RUST_LOG` | Standard Rust log filter fallback |

---

## Project Structure

```
kurd-mcp/
├── kurd/
│   ├── __init__.py            # Public API + __version__
│   ├── py.typed               # PEP 561 marker
│   ├── cli.py                 # `kurd serve` entry point
│   ├── router.py              # Router class + RuntimeConfig
│   ├── telemetry.py           # OpenTelemetry integration
│   ├── health_checks.py       # Readiness and liveness probes
│   ├── authorization.py       # RBAC helpers
│   ├── multitenancy.py
│   ├── billing.py
│   ├── idempotency.py
│   ├── dead_letter_queue.py
│   ├── secrets_management.py
│   ├── webhooks.py
│   ├── distributed_state.py
│   ├── distributed_tracing.py
│   └── ...
├── src/
│   └── lib.rs                      # Rust data plane (~3500 lines)
├── tests/
│   ├── test_core.py
│   ├── test_http_gateway.py        # Integration tests (module-scoped gateway)
│   ├── test_admin_api.py           # P1 — Admin HTTP API
│   ├── test_tool_filtering.py      # P2 — Per-tenant tools/list filtering
│   ├── test_tool_discovery.py      # P3 — Client-requested filter + _kurd metadata
│   ├── test_otel.py                # P4 — traceparent / OTLP export
│   ├── test_upstream.py
│   ├── test_load.py
│   ├── test_prometheus_metrics.py
│   └── upstream_server.py          # In-process upstream fixture
├── Cargo.toml
├── pyproject.toml
├── LICENSE
└── README.md
```

---

## Contributing

Issues and pull requests are welcome via the [GitHub repository](https://github.com/sn391/kurd).

Before submitting:

```bash
cargo check
cargo clippy -- -D warnings
maturin develop --release
python -m pytest -q
```

Please open an issue before starting large changes.

---

## License

[MIT](LICENSE) — Copyright © 2024 Semko Kermashani

---

*The name **Kurd** honors Kurdish identity and heritage. Bezhi Kurd u Kurdistan.*

