Skip to main content
Glama

Kurd MCP

PyPI Python License: MIT CI

Kurd is a high-performance Model Context Protocol (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

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

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:

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}}}'
{"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

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)

# 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

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:

router.reload_tool("search", new_search_function)

Unregistering

router.unregister_tool("search")

Introspection

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

Mounting Upstream Servers

Kurd aggregates remote MCP servers alongside local tools.

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

Upstream tools are prefixed with the upstream name:

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

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:

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

Search filter

Case-insensitive substring match across tool name and description:

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

Discovery metadata

Every tools/list response includes a _kurd object:

{
  "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

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

Or via the module-level API:

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

Manage upstream servers

# 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

# 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:

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:

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

Runtime status

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.

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):

KURD_AUTH_TOKEN=my-production-token kurd serve

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

IP allowlist

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

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

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

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 section.


Observability

Structured logging

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

router.configure_runtime(request_logging=True)

Control log verbosity via environment variable:

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

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

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

Datadog

# 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

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:

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

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 above for the full policy engine and tool-filtering setup.

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

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

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

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

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

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

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

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.

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

pip install maturin pytest

Build

# Development build (fast iteration)
maturin develop

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

# Release wheel
maturin build --release

Test

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

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.

Before submitting:

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

Please open an issue before starting large changes.


License

MIT — Copyright © 2024 Semko Kermashani


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