Skip to main content
Glama

mcp-gateway: a hardened MCP server framework for exposing internal tools to LLM clients

tests License: MIT Python MCP

Wrap the tools you want Claude Desktop, an IDE, or an agent to call in the controls a company needs before it lets an AI touch its systems: tenant isolation, per-tool auth and guardrails, rate limiting, audit logging, telemetry. MCP 2026-07-28 (and the 2025 revisions) over JSON-RPC 2.0, implemented here rather than imported, over stdio and HTTP.

Why this exists. An MCP server is an RPC surface into your company. The moment a model can call get_customer, everything behind that function is reachable by whoever holds the client — and by whatever the model was talked into. The reference servers are demos: they have no authentication, no notion of which tenant a caller belongs to, no limit on how often or how much a tool is called, no record of what happened, and they return whatever the tool returned, PII included. This is the production wrapper around those tools. The protocol is implemented in mcp_gateway/protocol.py and mcp_gateway/negotiation.py so every message is a pydantic model you can read, and the whole thing runs and tests offline.

The RPC-surface sibling of guardrail, which validates LLM inputs and outputs in general, and sql-guardrails, which makes LLM-generated SQL safe to execute. This one guards the boundary where a model calls your code.


Quick start

Everything below runs offline, with no API keys.

git clone https://github.com/darrshangovender/mcp-gateway && cd mcp-gateway
pip install -e ".[dev]"                 # pydantic + starlette; redis/otel are optional extras
python examples/crm_server.py demo      # two tenants, four tools, every control tripped once
python examples/client_demo.py          # a stdio client: server/discover, then 2026-07-28 calls
python examples/client_demo.py --legacy # the same server through the 2025 initialize handshake

The distribution is named agulhas-mcp-gateway (the plain name mcp-gateway is taken on PyPI); the import package is still mcp_gateway. It is not published yet, so install from a clone as above. Once it is, pip install agulhas-mcp-gateway will be the install line.

A gateway in one file, calling itself in-process:

import asyncio
import sqlite3

from pydantic import BaseModel

from mcp_gateway import APIKeyAuth, Gateway, RequestContext, ToolPolicy, tenant_filter, tool

db = sqlite3.connect(":memory:")
db.executescript("""
    CREATE TABLE customers (id INTEGER PRIMARY KEY, tenant_id TEXT, name TEXT, email TEXT);
    INSERT INTO customers VALUES (1, 'acme', 'Thandi Mkhize', 'thandi@example.co.za');
    INSERT INTO customers VALUES (2, 'globex', 'Sipho Dlamini', 'sipho@globex.example');
""")


class SearchIn(BaseModel):
    query: str


class SearchOut(BaseModel):
    rows: list[dict]


@tool("search_customers", input_model=SearchIn, output_model=SearchOut,
      scopes=("crm:read",), rate_tier="standard",
      policy=ToolPolicy(max_payload_bytes=2048, allowed_args=frozenset({"query"})))
def search_customers(args: SearchIn, ctx) -> SearchOut:
    tf = tenant_filter()                       # raises if no tenant is bound to the call
    cur = db.execute(f"SELECT id, tenant_id, name, email FROM customers "
                     f"WHERE {tf.sql} AND name LIKE ?", (*tf.params, f"%{args.query}%"))
    cols = [c[0] for c in cur.description]
    return SearchOut(rows=[dict(zip(cols, row)) for row in cur.fetchall()])


auth = APIKeyAuth(salt="change-me")            # in real use: salt and keys come from the environment
auth.add_key("acme-secret", tenant_id="acme", scopes=["tools:list", "tools:call", "crm:read"])

gateway = Gateway(tools=[search_customers], auth=auth)


async def main() -> None:
    meta = {"io.modelcontextprotocol/protocolVersion": "2026-07-28",
            "io.modelcontextprotocol/clientCapabilities": {}}
    request = {"jsonrpc": "2.0", "id": 1, "method": "tools/call",
               "params": {"name": "search_customers", "arguments": {"query": "a"}, "_meta": meta}}
    reply = await gateway.dispatcher.dispatch(request, RequestContext(api_key="acme-secret"))
    print(reply["result"]["structuredContent"])   # acme's row only; the e-mail address is redacted


asyncio.run(main())
# To serve it:  gateway.serve_stdio()   or   app = gateway.asgi()   then   uvicorn module:app

Prints {'rows': [{'id': 1, 'tenant_id': 'acme', 'name': 'Thandi Mkhize', 'email': '[EMAIL]'}]}.

Point Claude Desktop at a script that calls serve_stdio() with MCP_GATEWAY_API_KEY in its env, or run uvicorn and POST JSON-RPC to /mcp with Authorization: Bearer <key>. Bodies over 1 MiB are refused with HTTP 413 before parsing (gateway.asgi(max_body_bytes=...)). Browser Origins are refused with 403 unless listed (gateway.asgi(allowed_origins=[...])). GET /mcp/events is not part of MCP: it is this gateway's own per-tenant SSE feed of call outcomes.

Related MCP server: mcp-server-template

How it works

flowchart LR
    R[JSON-RPC request] --> V[protocol gate: era · version · _meta · HTTP headers]
    V --> A[auth: key → Principal]
    A --> S[scope check]
    S --> L[rate limit per tenant × tool]
    L --> G[guardrails: size · allow-list · deny_if]
    G --> T[bind TenantContext]
    T --> X[tool]
    X --> O[tenant ownership check]
    O --> P[PII scrub]
    P --> U[audit + span + SSE event]
    U --> Y[JSON-RPC response]
  1. The transport (stdio or HTTP) hands the dispatcher a raw message, the API key it found and, on HTTP, the request headers. It never interprets the message.

  2. The protocol gate decides whether this is a 2026-07-28 request (per-request _meta) or a 2025 one, checks the version and the required metadata, and on HTTP checks that MCP-Protocol-Version, Mcp-Method and Mcp-Name agree with the body. It runs before anything else, so a malformed request never reaches auth or a tool.

  3. The key resolves to a Principal(tenant_id, scopes, rate_tier). Keys are stored as salted SHA-256 and compared in constant time.

  4. tools:call plus the tool's own scopes are required; a missing scope is -32002 (-41002 on a 2026-07-28 request).

  5. A token bucket per (tenant, tool, tier) decides; exhaustion is -32003 / -41003 with retry_after.

  6. The tool's ToolPolicy checks payload size, argument allow-list and deny_if; any failure is -32001 / -41001.

  7. The tenant is bound in a contextvar, so concurrent calls cannot see each other's tenant, and the tool runs with validated arguments. On a 2026-07-28 request, arguments that fail validation come back as a tool result with isError: true that names the field and the rule, as that revision requires; on a 2025 request they are -32602.

  8. The result is validated against the output model, walked for rows tagged with a foreign tenant_id (-32004 / -41004 if any; the audit log records which tenant's rows leaked, the client is told only that the call was blocked), then scrubbed of emails, phone numbers and checksum-valid SA ID numbers, including when zero-width characters are hidden inside them. resources/read and prompts/get output goes through the same scrub.

  9. Every outcome (allowed, denied, errored, including a malformed tools/call and one rejected by the protocol gate) is appended to the audit log with a hash of the arguments, never the arguments, and the protocol revision it was served under. A tool's exception text is scrubbed before it is recorded.

Protocol: MCP 2026-07-28 and the 2025 revisions

The 2026-07-28 revision (spec, changelog) removes the initialize handshake and protocol-level sessions. A request is judged on its own: it carries the protocol version and the client's capabilities in params._meta. A default Gateway is dual-era: it serves 2026-07-28 and the older handshake revisions on the same stdio process or HTTP endpoint, chosen per request.

Revision

Served as

How the client opens

2026-07-28

modern, stateless

any request with _meta; optional server/discover probe

2025-11-25, 2025-06-18, 2025-03-26

legacy

initialize (echoes the version it serves, else answers with 2025-11-25); on HTTP a supported MCP-Protocol-Version header, or none for 2025-03-26

Gateway(supported_versions=[...]) narrows the list. Modern-only refuses initialize with an error that names the versions it speaks (the spec asks for this, because a legacy client has no other diagnostic); legacy-only answers a modern probe with UnsupportedProtocolVersionError, which is how a dual-era client knows to fall back to initialize. examples/client_demo.py implements that client side.

A 2026-07-28 request:

{"jsonrpc": "2.0", "id": 7, "method": "tools/call",
 "params": {"name": "search_customers", "arguments": {"query": "a"},
            "_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28",
                      "io.modelcontextprotocol/clientCapabilities": {},
                      "io.modelcontextprotocol/clientInfo": {"name": "my-client", "version": "1.0"}}}}

On HTTP the same request also carries MCP-Protocol-Version: 2026-07-28, Mcp-Method: tools/call and Mcp-Name: search_customers; a missing or disagreeing header is 400 with -32020.

Method

2026-07-28

2025 revisions

server/discover

yes (no key needed): versions, capabilities, identity, instructions

not applicable

initialize, notifications/initialized, ping

removed (-32601, HTTP 404)

yes

tools/list, tools/call

yes

yes

resources/list, resources/read, resources/templates/list (always empty)

yes

yes (templates list is new in 0.2.0)

prompts/list, prompts/get

yes

yes

What a 2026-07-28 reply carries that a 2025 one does not: resultType: "complete"; _meta["io.modelcontextprotocol/serverInfo"]; and on server/discover, tools/list, prompts/list, resources/list, resources/templates/list and resources/read, ttlMs (default 0, set with Gateway(cache_ttl_ms=...)) and cacheScope (public for discovery, private for everything else, because the data depends on the caller). POST /mcp replies also carry Cache-Control: no-store. tools/list returns tools in registration order, which is deterministic.

HTTP status codes follow the transport spec: 400 for a missing/invalid _meta (-32602), an unsupported version (-32022, with data.supported and data.requested) and a header mismatch (-32020); 404 with -32601 for an unknown or removed method; 403 for a refused Origin; 202 for a notification; 405 for GET/DELETE on /mcp. Mcp-Session-Id and Last-Event-ID are ignored and never minted. Everything else, including auth and tool errors, is 200 with the error in the body.

Error

2025 request

2026-07-28 request

guardrail denied

-32001

-41001

auth / scope denied

-32002

-41002

rate limited

-32003

-41003

tenant violation

-32004

-41004

unknown tool, resource or prompt; bad request params

-32602

-32602

unsupported protocol version / header mismatch

n/a

-32022 / -32020

The 2026-07-28 text forbids emitting -32002 (it was "resource not found") and asks implementations not to use -32000 to -32019 at all, so the four gateway codes move outside the JSON-RPC range on a modern request. Audit records and GatewayError.code keep the original values.

Not implemented, on purpose: subscriptions/listen and listChanged (the registry is fixed at start-up; the capability is not advertised), multi round-trip requests (the gateway never answers input_required), logging, completion, the Tasks extension, roots/sampling/elicitation, pagination (lists come back whole), x-mcp-header parameter mirroring, using _meta.traceparent as the parent of the call's span (it is accepted, not propagated), and MCP's OAuth authorization framework (API keys instead). notifications/cancelled is accepted and ignored.

The controls

Control

Module

What it enforces

On failure

Protocol gate

negotiation.py, versions.py

Era, version, required _meta, HTTP header/body agreement, no batching or null ids in 2026-07-28

-32602 / -32022 / -32020

API-key auth

auth.py

Hashed keys, constant-time compare, scopes, rate tier, revocation

-32002 / -41002

Tenant isolation

tenancy.py

tenant_filter() for data access; post-call check for foreign rows

-32004 / -41004

Guardrails

guardrails.py

Payload cap, argument allow-list, deny_if predicate, PII scrub on output

-32001 / -41001

Rate limiting

ratelimit.py

Token bucket per tenant × tool with named tiers; Redis or memory

-32003 / -41003

Audit

audit.py

Append-only JSONL: who, tool, args hash, outcome, latency, revision, reason

never skipped

Telemetry

telemetry.py

One span per call; OpenTelemetry if installed, no-op otherwise

—

Protocol

protocol.py

pydantic models for every message; -32700/-32600/-32601/-32602

per JSON-RPC

Threat model

STRIDE, applied to a gateway that sits between an MCP client (and the model behind it) and the tools of one organisation. The left column is the attack; the right two say what this code does about it and, just as important, what it does not. "Tested" means a test in tests/ fails if the behaviour is removed.

Trust boundaries. The client and the model are untrusted: the model can be talked into anything, and the client may be a stolen laptop. The tool code and the process the gateway runs in are trusted. The network is not, but TLS is the reverse proxy's job.

Threat

What the gateway does

What it does not do

S

A caller claims another tenant's identity

The tenant comes only from the API key's record, never from arguments or headers; tenant_filter() takes it from a contextvar (tested under concurrent calls from two tenants)

It cannot stop a tool from trusting a tenant_id argument its own author added

S

A key is guessed, replayed or stolen

Keys stored as salted SHA-256, compared in constant time over the whole list, revocable; per-key scopes and rate tier

No protection against theft from the client's config or the stdio env; no expiry or rotation tooling; no OAuth, mTLS or IP binding; a key has no human behind it

S

A web page rebinds DNS to reach a gateway on localhost

A request with an Origin header is refused with 403 unless the origin is on the allow-list (default: none); tested

A non-browser local process is unaffected; bind to 127.0.0.1 and keep key auth on

S

Forged protocol metadata (clientInfo, Mcp-* headers)

clientInfo is logged nowhere and decides nothing; headers must equal the body or the request is 400/-32020, so a proxy that routes or rate-limits on Mcp-Name cannot be fooled by a different body; tested

The gateway trusts whatever the proxy in front of it forwards

T

Crafted or oversized arguments

pydantic validation, argument allow-list, per-tool size cap, deny_if; 1 MiB body cap before parsing; tested

Injection inside a tool (hand-built SQL) is the tool author's; deny_if examples are blocklists, not proofs

T

Prompt injection carried in tool output

Nothing beyond PII scrubbing: output is data handed to the model

Not defended. Narrow tools and narrow scopes limit what an injected instruction can reach

T

Downgrade: omit _meta to land on the older path

The older path runs the same pipeline (auth, scopes, tenancy, guardrails, audit); supported_versions can switch it off; tested

Replies on that path have the older shape and the old error codes

R

"I never made that call"

Every outcome is appended with key id, tenant, tool, argument hash, outcome, latency and revision, including calls the protocol gate refused; tested

The log is append-only by convention, not tamper-evident (no hash chain or signature); a key id is not a person; arguments are hashed, so content cannot be reconstructed

I

One tenant reads another's rows

tenant_filter() plus a post-call walk of the result for rows tagged with a foreign tenant_id; the reply never names the other tenant; tested

Rows with no tenant_id field are not checked; a tool that aggregates across tenants is not caught

I

PII in results, resources, prompts or error text

Emails, South African phone formats and checksum-valid SA ID numbers are redacted, also when zero-width characters are hidden inside them; exception text is scrubbed before it reaches the audit log and clients see only the class name; tested

Names, addresses, other countries' IDs and free text pass through; dictionary keys are not scrubbed; this is regex plus checksums, not a DLP product

I

A cache serves one caller's data to another

2026-07-28 results are cacheScope: private except discovery, ttlMs defaults to 0, POST /mcp replies are Cache-Control: no-store; tested

If you set cache_ttl_ms, clients decide how they cache

I

Reconnaissance without a key

Nothing sensitive is returned before auth, but server/discover, initialize and /healthz answer without one

They disclose the server name and version, supported revisions, capabilities and instructions; /healthz also lists tool names. Put the HTTP transport behind network controls if those are sensitive

D

Call flooding

Token bucket per tenant × tool with tiers, shared across replicas with Redis; -32003/-41003 with retry_after; tested

Unauthenticated traffic is not limited (auth fails first); no per-IP limit; SSE connections are not capped; there is no per-call timeout, so a hung tool holds its slot, and on stdio requests run one at a time

D

Batch amplification

2025 batches are accepted, each element passes the limiter and, on HTTP, the whole body is capped; 2026-07-28 forbids batching and the gateway refuses it; tested

stdio has no line-length cap; the client that spawns it is trusted

E

Calling a tool beyond the key's scopes

tools:call plus the tool's own scopes are checked at call time; tools/list only hides tools as a convenience; tested

Scopes are flat strings (* is a root key); no per-argument authorisation beyond deny_if; a tool that holds its own credentials can be a confused deputy

E

A tool's bug leaks internals

A tool exception becomes isError: true with the class name only; unexpected failures become a bare -32603 and a server-side traceback; tested

The traceback is only as private as your log destination

Out of scope: TLS, host and container hardening, secret storage, and the model's own safety behaviour. The two runtime dependencies (pydantic, starlette) have not been audited for this README.

Results

Measured by python benchmarks/bench_pipeline.py on the machine named in the output, in-process, one trivial tool, one tenant, no network. It shows the gateway's own cost per tools/call, not your tools' cost, and the absolute figures will differ on other hardware, so read the rows against each other.

Measured on: Windows 10, 4 logical CPUs, Python 3.12.13, 2000 calls per row after warm-up.

Path

median (us)

p95 (us)

gateway overhead vs direct (us)

direct function call (no gateway)

8

15

tools/call, legacy request (dispatcher)

491

2,432

483

tools/call, 2026-07-28 request (dispatcher)

478

2,254

470

tools/call, 2026-07-28 over HTTP (httpx ASGI client)

5,382

148,899

5,374

So the whole pipeline (protocol gate, auth, scopes, rate limit, guardrails, tenancy, output validation, PII scrub, audit, event) costs about half a millisecond per call here, and the 2026-07-28 request is not measurably dearer than the old one. The HTTP row includes httpx's in-process ASGI client and the Windows timer tick, and the machine was shared while this ran, so its tail is mostly noise: treat the p95 columns as upper bounds and rerun on a quiet machine before quoting them.

Tests: 253, all offline (see below). Of those, 101 exercise the 2026-07-28 revision and dual-era behaviour directly (tests/test_protocol_2026.py, tests/test_http_2026.py, tests/test_stdio_2026.py), counted with pytest --collect-only.

Design decisions

Decision

Why

Own protocol layer, not the official SDK

Every message is a pydantic model in one file you can read, and the suite runs with no network and no SDK version drift.

Dual-era by default

Claude Desktop, IDEs and agent SDKs will not all move to 2026-07-28 on the same day. The gateway reads each request, so one deployment serves both and a client upgrade needs no gateway change.

No session state, even for the old revisions

The 2025 handshake is answered but remembers nothing, so any replica can serve any request. The cost is that a legacy HTTP request without the version header is taken to be 2025-03-26.

The gate runs before auth

A protocol fault is a client bug, not an access decision, and it should never touch a key lookup or a tool. The cost is that those faults are cheap to send unauthenticated (see the threat model).

Gateway error codes move on modern requests

-32002 is on the 2026-07-28 do-not-emit list. Remapping on the wire while the audit log keeps the old codes avoids breaking either side.

Bad tool arguments are results in 2026-07-28

The revision says input validation errors should reach the model as isError so it can retry. The message names the field and the rule, never the value sent.

server/discover needs no key

A client probes before it has chosen how to authenticate, and the answer is the same for everyone (cacheScope: public).

Origin is default-deny

The spec requires validating it. The only clients that send Origin are browsers, which is exactly the DNS-rebinding case.

Tool exceptions are results, not protocol errors

Per the MCP spec a failing tool returns isError: true; the class name goes to the client, the message goes to the audit log only.

Two layers of tenancy

tenant_filter() is what tools should use; the post-call walk catches the tool that forgot. Defence in depth for the failure that leaks the most.

Hash the arguments in the audit log

Arguments are where the PII lives. A hash still proves what was called and lets you correlate repeats.

Rate limiter degrades to memory

A limiter that takes the gateway down with Redis is worse than one that briefly limits per process. degraded is exposed on /healthz.

Checksums before scrubbing SA IDs

A 13-digit invoice reference is not an ID number. Luhn plus a valid date keeps the scrubber from redacting business data.

Limitations

  • API keys only. No OAuth (so none of MCP's authorization framework), no JWT, no per-user identity behind a key. A key is a tenant plus scopes; if you need user attribution, put it in the key id. A 2026-07-28 auth failure is 200 with an error body, not 401.

  • Protocol coverage is the tools/resources/prompts core. See "Not implemented" above: no subscriptions/listen, listChanged, multi round-trip requests, logging, completion, Tasks, pagination or x-mcp-header. Roots, sampling and elicitation are deprecated by the revision and not offered.

  • The three 2025 revisions share one code path and are not tested against each revision's full schema. Where they differ for this method set: batching is accepted on all of them (removed from 2025-06-18); structuredContent is sent to all of them (added in 2025-06-18); bad arguments are -32602 on all of them (2025-11-25 asks for isError); an unknown resource is -32602 on all of them (2025-11-25 and earlier used -32002).

  • No streaming tool results. A tool returns one value; long-running tools should return a handle.

  • stdio handles one request at a time and notifications/cancelled cannot interrupt a running tool. There is no per-call timeout on either transport.

  • The SSE feed at /mcp/events is not MCP. It is one-way, carries the endpoint event for clients written against the deprecated HTTP+SSE transport, and relays per-tenant call outcomes. It is the only GET stream; the MCP endpoint itself is POST-only and not resumable.

  • The PII scrubber is regex plus checksums, tuned for emails, South African phone formats and SA ID numbers. It will miss other identifiers and is not a substitute for not returning the column.

  • In-memory audit and rate limits are per process. Use JSONLAuditLog and RedisRateLimiter for anything with more than one replica.

  • Rate limits are per tenant, so unauthenticated traffic is not limited. Auth fails before the limiter runs; put the HTTP transport behind a reverse proxy with per-IP limiting if it is reachable from the internet. /healthz, server/discover and initialize are unauthenticated.

  • Unexpected exceptions are logged, not raised. Anything that is not a GatewayError becomes a bare -32603 for the client and a traceback on the mcp_gateway logger; configure logging or you will not see it.

Project layout

mcp-gateway/
├── mcp_gateway/
│   ├── protocol.py       # JSON-RPC 2.0 + MCP models, error codes, Dispatcher (guard + shaper hooks)
│   ├── versions.py       # supported revisions, _meta keys, which methods exist in which era
│   ├── negotiation.py    # ProtocolGate: per-request era, version, _meta and HTTP header checks
│   ├── auth.py           # APIKeyAuth · Principal · require_scope
│   ├── tenancy.py        # TenantContext · tenant_filter · assert_tenant_owned
│   ├── guardrails.py     # ToolPolicy · check_input · scrub_pii (Luhn SA ID, invisible-character aware)
│   ├── ratelimit.py      # Tier · TokenBucket · MemoryRateLimiter · RedisRateLimiter
│   ├── audit.py          # AuditRecord · JSONLAuditLog · hash_args
│   ├── registry.py       # @tool · @resource · @prompt · Registry
│   ├── telemetry.py      # get_tracer · OTelTracer · NoopTracer · RecordingTracer
│   ├── events.py         # per-tenant pub/sub feeding SSE
│   ├── server.py         # Gateway: the pipeline, discover/initialize, serve_stdio(), asgi()
│   └── transport/        # stdio.py (NDJSON) · http.py (POST /mcp, Origin check, GET /mcp/events)
├── examples/             # crm_server.py (SQLite CRM, two tenants) · client_demo.py (dual-era client)
├── benchmarks/           # bench_pipeline.py
├── tests/                # offline; *_2026.py files cover the 2026-07-28 revision
├── SECURITY.md · CHANGELOG.md · CITATION.cff
├── Dockerfile · docker-compose.yml · .env.example · Makefile

Tests

pytest tests/             # 253 tests, no network, no Redis, no OpenTelemetry needed
ruff check .
make bench                # the Results table above

Protocol conformance for 2026-07-28 (server/discover; required _meta; version negotiation and the supported list; removed methods; null ids and batching refused; resultType, ttlMs, cacheScope; the code remap and the codes the revision forbids; HTTP Mcp-* header validation including the base64 form, 400/404/405/403 statuses, no sessions) and for the older revisions on the same gateway (initialize echo and fallback, headerless HTTP, batches, the old codes), plus the earlier suites: auth (missing/wrong key, insufficient scope, revocation), tenancy (cross-tenant read blocked, contextvar isolation under concurrent calls from two tenants, violation replies that name no other tenant), guardrails (oversized payload, allow-list, deny_if, PII scrub on tool, resource and prompt output, zero-width characters), rate limiting (refill maths, tier separation, Redis fallback with a fake client), audit (every outcome logged including malformed calls, arguments hashed, exception text scrubbed), HTTP via httpx.ASGITransport and the SSE stream, and stdio through real pipes under a cp1252 console. The README's code block and examples/client_demo.py are run by the suite. CI runs the suite and ruff check . on Python 3.11 and 3.12.


Built by Darrshan Govender · Agulhas Code

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      C
      maintenance
      Exposes internal databases, HTTP APIs, and files to LLM agents over MCP, with auto-generated schemas, access controls, rate limiting, redaction, and audit logging.
      3
      MIT
    • A
      license
      A
      quality
      C
      maintenance
      Enables developers to expose product operations as model-callable tools over stdio or Streamable HTTP with bearer-token auth, per-client rate limits, host allow-listing, structured call logging, and a swappable backend for secure, testable deployments.
      3
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables AI agents to safely invoke enterprise tools such as Supabase PostgreSQL and the Notion API through a zero-trust gateway that performs server-side policy and risk evaluation, cryptographically binds approval tickets to exact parameters, and requires out-of-band human approval for destructive operations. It also scrubs credentials from logs and responses while providing audit trails, rate limiting, and a web console for reviewing pending authorizations.
      Apache 2.0
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables AI agents to call backend tools under per-agent policy, deciding which tools each agent can see, what arguments it may pass, how often it may call, and which writes wait for human approval. It also redacts secrets and PII from tool results, fences untrusted output against injection, and records every call in a hash-chained audit log.
      19 npm
      MIT