{"slug": "mcp-gateway-what-it-adds-over-a-raw-mcp-server", "title": "MCP Gateway: What It Adds Over a Raw MCP Server", "summary": "A developer detailed the design of an MCP gateway layer that sits between an agent host and a raw MCP server, adding per-key and per-team rate limits, budget caps, and one audit row per tool call. The writeup walks through middleware that carves out /api/mcp from the generic API rate limiter so tool traffic is metered by API key rather than IP, and an audit enrichment step that labels calls as \"mcp\" or \"rest\" at write time. The author argues a plain proxy cannot answer who called, how often, at what cost, or which log row proves it.", "body_md": "**Short answer:** An MCP gateway is the layer between an agent host and the MCP server. It adds\n\nwhat a raw server does not have: one upstream URL for every client, per-key and per-team\n\nrequest limits, budget caps clamped to the plan, and one audit row per tool call. A plain proxy\n\nforwards the same bytes and can describe none of it.\n\n**Key takeaways**\n\n`tools/list` and\n`tools/call`; it has no opinion about who may call, how often, or at what cost.\nAn MCP server answers questions about tools. A gateway answers questions about *callers*: which key\n\nis this, how many requests has it sent this minute, what has this team spent this month, and which\n\nrow in the audit log proves the call happened. Those are four questions with four data sources, and\n\nnone of them belong inside a tool implementation.\n\nThe demand for that layer is measurable rather than assumed. The phrase carries roughly **2,400 US searches a month** with a keyword difficulty of \n\n`ai gateway` at`llm gateway` at `mcp proxy` at `mcp cli` at SmartGate is one implementation of that layer — an MCP-native algorithm gateway for token control,\n\ntraffic shaping, and agent audit. Hosts point at one Streamable HTTP endpoint and get seven tools:\n\nsmart_fetch, smart_search, smart_context_gate, smart_dedup, smart_budget_guard, smart_memory and\n\nsmart_pipe. The twelve excerpts below are the parts of that path a raw MCP server simply does not\n\nhave.\n\nThe first decision any request meets is a path test at the edge, and the interesting part is what\n\nis excluded from it:\n\n```\n# middleware.ts — source lines 72–89 (middleware)\nasync function middleware(request: NextRequest) {\n  const { pathname } = request.nextUrl;\n\n  // 1) API routes → rate-limit + feature gate (always bypass intl/auth below)\n  if (pathname.startsWith(\"/api/\")) {\n    if (\n      !pathname.startsWith(\"/api/webhooks/\") &&\n      !pathname.startsWith(\"/api/mcp\")\n    ) {\n      const rateLimitResponse = await rateLimitApi(pathname, request);\n      if (rateLimitResponse) return rateLimitResponse;\n\n      const apiBlock = getDisabledApiResponse(pathname);\n      if (apiBlock) return apiBlock;\n    }\n\n    return NextResponse.next();\n  }\n```\n\nThree things are decided in eighteen lines. Traffic under `/api/` is rate-limited and\n\nfeature-gated before it reaches a handler — that is the generic API budget. Two prefixes are\n\ncarved out of it: `/api/webhooks/` for provider callbacks that must not be throttled, and\n\n`/api/mcp`, the tool-call path. The carve-out is the design statement of this whole page: MCP\n\ntraffic is not charged against the API request budget, because it has its own limiter keyed by the\n\nAPI key rather than by IP. Anything outside `/api/` falls through to the page logic below, so a\n\ntool call never touches locale or session middleware. A plain proxy has no equivalent branch — it\n\napplies one limit to every path, because it has no vocabulary for \"this path is metered\n\ndifferently\".\n\nOnce a call is admitted it has to be attributable, and attribution happens at write time:\n\n```\n# backend/smartgate/core/audit_enrichment.py — source lines 35–40 (infer_route)\ndef infer_route(path: str, route_hdr: str) -> str:\n    if route_hdr:\n        return route_hdr\n    if path.startswith(\"/mcp\"):\n        return \"mcp\"\n    return \"rest\"\n```\n\nTwo lines of policy. A header wins if present, which lets an edge declare a route it knows better\n\nthan the server can infer. With no header the path decides: anything under `/mcp` is labelled\n\n`mcp`, everything else `rest`. The fallback is a constant rather than an empty string, so old rows\n\ncollect in one bucket instead of scattering across nulls.\n\nThat matters more for an AI gateway than a classic one, because the audit row is part of what you\n\nare buying. A gateway that terminates both a REST surface and an MCP endpoint and labels neither\n\nproduces logs that cannot answer \"how much of this month's traffic was agent traffic\". Deriving\n\nthe label later from route tables is the version that breaks quietly, on the day a route is\n\nrenamed.\n\nThe limit that refuses a runaway agent is a pair of counters in a shared store — one per key, one\n\nper team:\n\n```\n# backend/smartgate/core/rate_limiter.py — source lines 51–77 (check_rate_limit_mcp)\nasync def check_rate_limit_mcp(\n    key_id: str,\n    team_id: str,\n    *,\n    per_key_limit: int,\n    team_ceiling: int,\n    window_seconds: int = 60,\n) -> dict:\n    try:\n        redis = await get_redis()\n    except Exception as exc:\n        logger.warning(\"mcp rate_limit fail-open: %s\", exc)\n        return {\"allowed\": True, \"fail_open\": True}\n\n    try:\n        bucket = int(time.time() / window_seconds)\n        key_bucket = f\"rate_limit_mcp:{key_id}:{bucket}\"\n        team_bucket = f\"rate_limit_mcp_team:{team_id}:{bucket}\"\n\n        key_count = await _incr_bucket(redis, key_bucket, window_seconds)\n        if key_count > per_key_limit:\n            ttl = await redis.ttl(key_bucket)\n            return {\n                \"allowed\": False,\n                \"retry_after\": max(1, ttl),\n                \"limit_scope\": \"mcp_key\",\n            }\n```\n\nThe counting is deliberately boring: one integer per key per time bucket, incremented on entry and\n\ncompared against the plan's number. What makes it diagnosable is the refusal payload — every\n\nrejection carries `retry_after` from the bucket's own TTL and a `limit_scope` naming which wall\n\nwas hit, `mcp_key` or `mcp_team`. A client that gets a scoped 429 can back off correctly; one that\n\ngets a bare 429 retries in a loop and makes the incident worse.\n\nThe second branch is the team ceiling, the one people forget to model: per-key limits alone mean a\n\nfleet of ten keys has ten times the allowance. The counters are incremented in sequence, key\n\nfirst, so a single key cannot consume more than its share of the team's.\n\nSizing a bucket that several callers share at once is a concurrency decision rather than a rate\n\ndecision, and [Concurrency Control in AI Backend Systems](https://smartgate.network/integration/concurrency-control-in-ai-backend-systems)\n\ncovers the queueing and pool side of it.\n\nThen the honest part. If the counter store cannot be reached, the function returns `allowed: True`\n\nwith `fail_open: True` rather than refusing every call in the fleet — an availability choice, and\n\none that is visible in the return value instead of hidden in a log line.\n\nRate limits and spend caps are different resources, and the second is computed from two objects\n\nrather than typed into a config file:\n\n```\n# lib/settings/clamp-gateway-policy.ts — source lines 4–35 (clampGatewayPolicyToPlan)\nfunction clampGatewayPolicyToPlan(\n  policy: GatewayPolicyV1,\n  caps: PlanCapabilities,\n): GatewayPolicyV1 {\n  const rpm = Math.min(policy.playground.rpm, caps.maxPlaygroundRpm);\n\n  let dailyCap = policy.playground.daily_request_cap;\n  if (caps.minPlaygroundDailyCap != null) {\n    dailyCap = caps.minPlaygroundDailyCap;\n  } else if (\n    dailyCap != null &&\n    caps.maxPlaygroundDailyCap != null\n  ) {\n    dailyCap = Math.min(dailyCap, caps.maxPlaygroundDailyCap);\n  }\n\n  return {\n    ...policy,\n    playground: {\n      rpm,\n      daily_request_cap: dailyCap,\n    },\n    governance: {\n      allow_member_tool_overrides: caps.memberToolOverridesAllowed\n        ? policy.governance.allow_member_tool_overrides\n        : false,\n      allow_integrator_hmac_budget: caps.l2HmacBudgetAllowed\n        ? policy.governance.allow_integrator_hmac_budget\n        : false,\n    },\n  };\n}\n```\n\nThe shape is the useful part. Effective requests per minute is the smaller of what the team asked\n\nfor and what the plan allows, so a policy written under a larger plan cannot survive a downgrade\n\nas a promised rate. The daily cap has three cases: a plan that sets a floor wins outright,\n\notherwise the team's cap is clamped to the plan maximum, otherwise it passes through untouched.\n\nTwo governance switches — member-level tool overrides and per-integrator HMAC budgets — are forced\n\nto `false` when the plan does not grant them.\n\nEverything here is pure: policy plus capabilities in, policy out, no store read. That makes it\n\ncallable on the request path and in an admin screen with the same answer — and it is exactly the\n\ncomputation a traffic proxy has nowhere to put.\n\nThe numbers the limiter enforces are read from one place, so a plan change does not become a code\n\nchange:\n\n```\n# lib/plan-rate-limits.ts — source lines 5–12 (getPlanRateLimits)\nfunction getPlanRateLimits(plan: Plan) {\n  const f = getPlanCatalogEntry(plan).features;\n  return {\n    restWriteRpm: f.max_team_rpm,\n    mcpRpmPerKey: f.mcp_rpm_per_key,\n    mcpRpmTeamCeiling: f.mcp_rpm_team_ceiling,\n  };\n}\n```\n\nThree values come out: the REST write ceiling for a team, the MCP rate per key and the MCP team\n\nceiling. The asymmetry is deliberate — the protocol with autonomous callers gets two ceilings,\n\nbecause the caller that throttles itself politely and the caller that retries in a loop are the\n\nsame caller at different hours. The REST side keeps one.\n\nNote where this lives: a catalog lookup, not a per-request computation. The plan catalog is the\n\nsingle source for what a plan may do, and the limiter only ever compares measured counts to these\n\nnumbers. When the two disagree, the catalog wins, because it is the object a billing change edits.\n\nThis is the shortest excerpt here and the one that separates a gateway from a proxy:\n\n```\n# lib/settings/load-gateway-policy.ts — source lines 10–12 (loadGatewayPolicy)\nfunction loadGatewayPolicy(team: TeamPolicySource): GatewayPolicyV1 {\n  return parseGatewayPolicy(team.gatewayPolicy);\n}\n```\n\nThree lines: take the team record, parse its gateway policy, return it. The function is\n\nuninteresting; *when* it runs is the product. It runs on the request path, against the team the\n\nauthenticated key resolved to, so the policy is per tenant and changes without a deploy or a\n\nrestart.\n\nA proxy can do none of that. It sees bytes, a method and a destination, so the only limits it can\n\nenforce are the ones expressible in those terms — requests per second, bytes per second, IP\n\nallowlists. The moment the correct limit is \"this key, this team, this month, in tokens\", the\n\nproxy must be told by something that parsed the request. That something is the gateway, and this\n\nis where it reads its instructions.\n\nPolicy objects arrive from a database column, which means they arrive in every shape a column can\n\nhold:\n\n```\n# lib/settings/plan-seeds.ts — source lines 15–21 (isEmptyPolicy)\nfunction isEmptyPolicy(raw: unknown): boolean {\n  if (raw === null || raw === undefined) return true;\n  if (typeof raw === \"object\" && Object.keys(raw as object).length === 0) {\n    return true;\n  }\n  return false;\n}\n```\n\nNull, undefined and an empty object `{}` are one answer; anything else is another. The\n\ndistinction the caller needs is not \"was this falsy\" but \"has this team ever configured a policy\"\n\n— because an empty policy is the normal state of a new team and a seed path can fill it, while a\n\nthrown error on the same input turns normal onboarding into a broken page.\n\nThe three-line shape is also a small contract with the rest of the file: `isEmptyPolicy` is the\n\nonly place that decides emptiness, so the seed logic and the admin form agree about when a team is\n\nunconfigured. Two different emptiness tests — one for the UI, one for the request path — is how a\n\nteam ends up with a policy it can see but the gateway does not apply.\n\nClient configuration is where an MCP gateway earns the \"one upstream URL\" claim:\n\n```\n# lib/connect/mcp-config-templates.ts — source lines 249–264 (mcpConfigFilename)\nfunction mcpConfigFilename(platform: McpPlatform): string {\n  switch (platform) {\n    case \"Claude Desktop\":\n      return \"claude_desktop_config.json\";\n    case \"Cursor\":\n      return \"mcp.json\";\n    case \"Windsurf\":\n      return \"mcp_config.json\";\n    case \"Hermes\":\n      return \"mcp.json\";\n    case \"OpenClaw\":\n      return \"openclaw-smartgate-snippet.json\";\n    default:\n      return \"smartgate-mcp.json\";\n  }\n}\n```\n\nA switch over the platform returning a filename per host — `claude_desktop_config.json` for Claude\n\nDesktop, `mcp.json` for Cursor, `mcp_config.json` for Windsurf, a namespaced snippet for OpenClaw,\n\nand a default for anything else. The URL, the transport and the auth header are identical in every\n\ncase; only the file to edit changes.\n\nThat is the operational difference between a hosted gateway and a local stdio server. A stdio\n\nserver is a process on the caller's machine: every host needs its own copy, its own runtime and\n\nits own upgrade path. A hosted endpoint is one URL plus one credential, and the host's config\n\nformat is the only variable left. The function is small because the interesting engineering —\n\ntransport, session handling, normalization — happens on the other side of that URL. That other side\n\nis worth reading once: [actors, lifecycle and transport](https://smartgate.network/industry/model-context-protocol-explained)\n\ncovers the handshake, the capability exchange, and why stdio and Streamable HTTP serve different\n\ndeployments.\n\nThe policy object is the security boundary, so writes to it are gated before they are validated:\n\n```\n# lib/settings/assert-plan-allows.ts — source lines 40–44 (assertGatewayPolicyEditable)\nfunction assertGatewayPolicyEditable(caps: PlanCapabilities) {\n  if (!caps.teamGatewayPolicyEditable) {\n    throw new PlanFeatureDisabledError(\"PRO\", \"gateway_policy\");\n  }\n}\n```\n\nOne guard, one error type. If the plan does not grant an editable team policy, the caller gets\n\n`PlanFeatureDisabledError(\"PRO\", \"gateway_policy\")` instead of a silent no-op — the error names\n\nboth the missing capability and the protected object.\n\nThe reasoning is worth stating plainly, because \"who can edit the limits\" decides whether the\n\nlimits mean anything. Per-key limits, budget caps and governance switches all live in one object\n\nthe request path reads, so any member who can write that object can raise their own ceiling and\n\nturn enforcement into a suggestion. The write path is therefore plan-gated, and the check runs\n\nbefore the mutation rather than after: a rejected edit leaves the previous policy in force, which\n\nis the fail-closed direction a security control should fail in.\n\nTool registration is declarative, and the interesting claim is about the function body:\n\n```\n# backend/smartgate/api/mcp.py — source lines 128–148 (smart_search)\n@server.tool(\n        name=\"smart_search\",\n        description=TOOL_DESCRIPTIONS[\"smart_search\"],\n        annotations=tool_annotations(\"smart_search\"),\n    )\n    async def smart_search(\n        query: str = Field(description=\"Search query string.\"),\n        max_results: int = Field(\n            default=10,\n            description=\"Maximum number of results to return (1–50).\",\n        ),\n    ) -> str:\n        _, registry = _app_state()\n        module = registry.get(\"search\")\n        ctx = _tool_ctx()\n        return await _run_with_audit(\n            \"search\",\n            ctx,\n            module.process(ctx, query=query, max_results=max_results),\n            {\"query\": query},\n        )\n```\n\nEach tool is declared once, with its description read from a shared table and its annotations\n\nderived from the tool name, so the model-facing metadata and the documentation cannot drift apart.\n\nThe body then does what every other tool does: resolve the module from the registry, build a call\n\ncontext, run the coroutine, and return through `_run_with_audit` with the arguments that should be\n\nrecorded.\n\nThat shared ending is the gateway's actual product. Because all seven tools leave through one\n\nfunction, a missing audit row means the call never reached the gateway rather than that one tool\n\nforgot to log — and there is one place to hold the budget check, the error translation and the\n\ntrace id. A raw MCP server hands you the tool surface and leaves that endpoint design to you.\n\nEvery limit on this page is a counter in a store, and the store is resolved rather than assumed:\n\n```\n# lib/redis/config.ts — source lines 26–29 (isTcpRedisConfigured)\nfunction isTcpRedisConfigured(): boolean {\n  const url = redisUrl();\n  return Boolean(url && (url.startsWith(\"redis://\") || url.startsWith(\"rediss://\")));\n}\n```\n\nA URL test, two schemes, one boolean. `redis://` and `rediss://` are the TCP forms; anything else\n\n— including the HTTP-based serverless Redis some deployments use instead — is not what this check\n\nasks about. Keeping the test narrow lets the caller tell \"no counter store is configured\" apart\n\nfrom \"a counter store is configured in a shape this path does not speak\".\n\nThe client-side consequence is why this matters on a page about gateways. Whatever the host is — a\n\nVS Code extension, a desktop app, a CLI agent — the counter it is measured against lives on the\n\nserver. Two hosts calling the same key share one budget, limits survive a host restart, and\n\nreinstalling the client does not grant a fresh allowance. Local enforcement cannot promise any of\n\nthat.\n\nResolution order matters when the store is missing, because it decides whether limits hold:\n\n```\n# lib/redis/config.ts — source lines 38–42 (resolveRedisMode)\nfunction resolveRedisMode(): RedisMode {\n  if (isUpstashRestConfigured()) return \"upstash\";\n  if (isTcpRedisConfigured()) return \"tcp\";\n  return \"none\";\n}\n```\n\nTwo checks in priority order and a constant: an Upstash-style REST configuration wins, then a TCP\n\nURL, then `none`. The first two are live limiters; the third is the mode in which the limiter's\n\nfail-open branch is the only branch that runs, and every call is admitted.\n\nState that as a limitation rather than a feature. A remote MCP server with no counter store still\n\nanswers — availability first — but its per-key limits become advisory and the budget guard has\n\nnothing to count against. The audit rows still appear, which is the useful part: a deployment that\n\ndegraded to `none` can be found afterwards from what the rows do not contain, instead of from a\n\nspend surprise.\n\nEach alternative is described the way its own documentation describes it; the links are sources,\n\nnot claims from this page.\n\nVendors describe this layer with different scopes, and the difference is instructive. Microsoft's\n\nMCP Gateway project calls itself \"a reverse proxy and management layer for MCP servers\", aimed at\n\nsession-aware routing and lifecycle management\n\n([Microsoft](https://microsoft.github.io/mcp-gateway/)). Kong describes an MCP gateway as\n\ncentralising \"access, security, and management for multiple Model Context Protocol servers\"\n\n([Kong](https://konghq.com/blog/learning-center/what-is-a-mcp-gateway)). Both descriptions are\n\nconsistent with this page, and both emphasise the routing and lifecycle half — the half a\n\nwell-built proxy can reach. The half these excerpts show is per-caller accounting: which key\n\ncalled, how often, at what spend, and the row that records it.\n\n|  | What it governs | Where the limits live | What you pay | \n|---|---|---|---|\n| **A raw MCP server (stdio)** | The tools it implements; nothing about the caller | Nowhere — the process trusts the host that launched it | Host machine time and your own engineering | \n| **An MCP server behind a plain reverse proxy** | Forwarding, TLS, maybe an IP-level request cap | In the proxy configuration, blind to tool calls | The proxy tier, plus the metering you still build | \n| **A model-proxy AI gateway** ([Cloudflare](https://developers.cloudflare.com/ai-gateway/) ,[Kong](https://developer.konghq.com/ai-gateway/) ) | Model traffic, per each vendor's own documentation | In the vendor's console or the proxy's config file | A subscription or a per-token fee; agent tool calls often stay outside its view | \n| **SmartGate** — an MCP-native algorithm gateway | Which tools a key may call, how fast, how much it may spend, and the row that proves each call | In one policy object, read per request and clamped to the plan | Platform fee, plus a share only after measured savings pass the threshold | \n\nThe billing model is the part worth testing against your own invoices: *pay for the platform, share only when you save*. Free is $0 with 2M tokens a month, all seven tools, 120 MCP requests a\n\n**Does an MCP gateway replace the MCP server?**\n\nNo. The server owns the tools — what they do, what arguments they take, what they return. The\n\ngateway owns the caller: identity, rate, budget and the audit record. Removing the gateway leaves\n\na working server with no enforcement; removing the server leaves a gateway with nothing to route.\n\n**Is an MCP gateway the same thing as an MCP proxy?**\n\nA proxy moves bytes between a client and a server. A gateway parses enough of the request to\n\nprice it, attribute it and refuse it. Every gateway can proxy; the reverse is not true, because\n\nper-key and per-month limits need fields a byte pipe never reads.\n\n**Where do the per-key limits actually apply?**\n\nIn the gateway, against the key that authenticated the request, before the tool runs. Two hosts\n\nsharing one key share one allowance, and the team ceiling is checked after the per-key window, so\n\none key cannot consume the whole team's budget in a burst.\n\n**What happens to limits if the counter store goes down?**\n\nThe limiter fails open: calls are admitted with a flag naming the degraded path. That is a choice\n\nfor availability over enforcement, and it is visible in the response rather than buried in logs —\n\nso a degraded window is something you can find afterwards.\n\n**Do budget caps need a code change when a plan changes?**\n\nNo. The effective rate and daily cap are computed as the minimum of the team policy and the plan\n\ncapability, and the governance switches are forced off when the plan does not grant them. Editing\n\nthe plan catalog moves every team bound by it.\n\n**Which clients work without a local server process?**\n\nAny host that speaks Streamable HTTP over a URL. The gateway ships per-host configuration blocks\n\nfor the common ones, but the transport is the standard one — nothing here requires a process on\n\nthe caller's machine.\n\n`rest` label.\nThe code in this article is not transcribed. Each block was cut directly out of the slice body\n\nreturned by the SmartGate slice API and re-asserted byte-for-byte as a substring of that body\n\nbefore publication; the first line inside every fence records the file and the exact source lines.\n\nSymbols were pinned by whole-name containment (rule A level 2) and confirmed by the service's\n\nslot-proof endpoint before any prose was written. Ten of the twelve excerpts are whole slice\n\nbodies; the `middleware` and `check_rate_limit_mcp` blocks are windows into longer files, and the\n\nlines around them are described rather than quoted.\n\nDemand figures come from this project's own keyword run, recorded in `research_brief.md` and\n\n`search_volume.json`: the section phrases `ai gateway` (2,400), `llm gateway` (1,600),\n\n`mcp proxy` (720), `mcp cli` (480), `ai agent security` (480), `openclaw mcp` (480),\n\n`vscode mcp` (390), `remote mcp server` (390), `mcp router` (260), `model gateway` (70),\n\n`api gateway for ai` (20) and `mcp firewall` (20), each with its competition band; the head term\n\nand its difficulty were measured in the same research pass.\n\n| # | SERP keyword | Symbol | File | Source lines | How it was pinned | sha256(12) | \n|---|---|---|---|---|---|---|\n| 1 | mcp router | `middleware` | `middleware.ts` | 72–89 | rule A L2 → slot-proof | `9ab198bee98b` | \n| 2 | api gateway for ai | `infer_route` | `backend/smartgate/core/audit_enrichment.py` | 35–40 | rule A L2 → slot-proof | `8cb200979cc6` | \n| 3 | mcp firewall | `check_rate_limit_mcp` | `backend/smartgate/core/rate_limiter.py` | 51–77 | rule A L2 → slot-proof | `b540f8154beb` | \n| 4 | ai gateway | `clampGatewayPolicyToPlan` | `lib/settings/clamp-gateway-policy.ts` | 4–35 | rule A L2 → slot-proof | `613facc8545b` | \n| 5 | llm gateway | `getPlanRateLimits` | `lib/plan-rate-limits.ts` | 5–12 | rule A L2 → slot-proof | `6d29a819fd91` | \n| 6 | mcp proxy | `loadGatewayPolicy` | `lib/settings/load-gateway-policy.ts` | 10–12 | rule A L2 → slot-proof | `82918a7047cc` | \n| 7 | model gateway | `isEmptyPolicy` | `lib/settings/plan-seeds.ts` | 15–21 | rule A L2 → slot-proof | `142457b5325c` | \n| 8 | mcp cli | `mcpConfigFilename` | `lib/connect/mcp-config-templates.ts` | 249–264 | rule A L2 → slot-proof | `d60503ebfb68` | \n| 9 | ai agent security | `assertGatewayPolicyEditable` | `lib/settings/assert-plan-allows.ts` | 40–44 | rule A L2 → slot-proof | `5bf979730866` | \n| 10 | openclaw mcp | `smart_search` | `backend/smartgate/api/mcp.py` | 128–148 | rule A L2 → slot-proof | `12366ba0d241` | \n| 11 | vscode mcp | `isTcpRedisConfigured` | `lib/redis/config.ts` | 26–29 | rule A L2 → slot-proof | `7ca086a475d5` | \n| 12 | remote mcp server | `resolveRedisMode` | `lib/redis/config.ts` | 38–42 | rule A L2 → slot-proof | `3a9dd8e9daf4` | \n\nEvery fenced block above was cut from the slice body and re-asserted against it byte-for-byte before\n\npublication. 12 of 12 sections pinned, 0 abstentions, 0 misses.", "url": "https://wpnews.pro/news/mcp-gateway-what-it-adds-over-a-raw-mcp-server", "canonical_source": "https://dev.to/smartgate/mcp-gateway-what-it-adds-over-a-raw-mcp-server-2dnh", "published_at": "2026-09-26 15:18:16+00:00", "updated_at": "2026-09-26 15:59:04.080206+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "ai-infrastructure", "developer-tools", "mlops"], "entities": ["MCP", "SmartGate", "Next.js"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/mcp-gateway-what-it-adds-over-a-raw-mcp-server", "markdown": "https://wpnews.pro/news/mcp-gateway-what-it-adds-over-a-raw-mcp-server.md", "text": "https://wpnews.pro/news/mcp-gateway-what-it-adds-over-a-raw-mcp-server.txt", "jsonld": "https://wpnews.pro/news/mcp-gateway-what-it-adds-over-a-raw-mcp-server.jsonld"}}