Skip to main content
Glama

hq-mcp

Russian version · English

MCP server that gives an AI agent access to a VPN business: the billing SHM and the panel Remnawave, stitched together so that a question spanning both systems can be answered with a single call.

No tool changes anything with the same call that requested it: a writing tool first returns a plan, and applying it is a second call carrying that plan's identifier.

How it looks in practice

The first call on any installation is platform_probe. It answers what exists in this deployment and what of it is alive; everything else here derives from what it reports. The answers below are trimmed, the values are fictional.

platform_probe {}
{
  "shm":   { "configured": true, "reachable": true, "version": "2.19.4", "live": true },
  "remna": { "configured": true, "reachable": true, "version": "3.2.3",
             "runtime": { "instances": 6, "youngestUptimeSeconds": 54294 } },
  "capabilities": { "shm.filter": false, "remna.realtimeBandwidth": true,
                    "tunnel.mysql": false, "…": "…" },
  "warnings": [{ "code": "specs_are_stale", "message": "…" }]
}

Next is a question that neither system answers alone: “the client writes that they paid, but there is no config.”

client_resolve { "query": "kot@example.com" }
{
  "shm":   { "count": 1, "matches": [{ "user_id": 4821, "email": "kot@example.com",
                                      "blocked": false }] },
  "remna": { "count": 0, "ambiguous": false,
             "paths": [{ "path": "email",   "tried": true, "found": 0, "note": null },
                       { "path": "service", "tried": true, "found": 0, "note": "…" }] }
}

The panel knows nothing about him, but count: 0 here is not “no account”: paths names each search performed and what it does not see. What actually happened is told by the second pair of eyes:

provisioning_diagnose { "shm_user_id": 4821 }
{
  "verdict": "panel_user_missing",
  "services": { "items": 1, "diagnosed": [{
    "user_service_id": 90210,
    "status": "ACTIVE",
    "verdict": "panel_user_missing",
    "storage": { "name": "vpn_mrzb_90210", "present": true, "checked": true },
    "panel":   { "username": "HQVPN_90210", "id": 11274, "found": false, "checked": true },
    "spool":   { "total": 0, "stuck": 0, "failed": 0, "succeeded": 0 },
    "history": { "total": 1, "success": 1 }
  }] }
}

The service is ACTIVE, the configuration snapshot is in place, provisioning reported success — but the user to whom that success belongs is not in the panel. Neither billing nor panel alone shows this.

There are thirty-four tools that read both systems. The rw mode adds fifteen writing ones: thirteen change real data, one applies a plan, and another reads the local mutation log.

Related MCP server: xendit-mcp

Why composite tools instead of endpoint proxies

The obvious design is one tool per HTTP endpoint, about a hundred and fifty of them. It was written and thrown away, for two reasons.

A raw proxy nullifies any denylist. If the model can call GET <any path>, then the list of operations you decided not to allow is decoration: the forbidden path is one line away. Here tools call named routes by name, and a build-time scanner fails the run if a forbidden path appears as a literal in the source.

And an endpoint is not a question. The example above touches four SHM routes and two panel routes, and the interesting part is precisely the seam. client_overview, sync_audit, and provisioning_diagnose exist because bugs live on that seam.

The rule that defined everything else

An empty answer must never be taken as proven absence.

When a backend fails, the tool degrades: the failure goes into degraded, the partial_result warning names the missing half, and any finding that depended on that half is suppressed, not computed from what survived. When a list is truncated, the server total arrives with it — so that “no such service” does not rely on an undeclared window.

This is not theoretical caution. During development, one tool read all panel records, discarded them all because a field had been renamed on that side, and then reported that hundreds of clients needed reprovisioning — a destructive recommendation, stated confidently and derived from an empty set. The fix was not just the renamed field: it was that a basket computed from an unusable input must refuse to be a finding.

Compatibility: will it work for you

Tested on SHM 2.19.4 and Remnawave 3.2.3 — both numbers taken from a working deployment, not from the specification.

Minimum is SHM 2.18.0 and Remnawave 3.0.0. The official danuk/shm works: all routes the tools call are upstream, no fork needed. The only place where that deployment's patch was visible to the tool is the fourth flag GET /user/password-auth; now its absence is called a sign_in_flag_absent warning, not passed off as a diagnosis. The full list of routes for both systems, the version each appeared in, and a detailed answer about the fork are in COMPATIBILITY.md.

The check takes one call — the same platform_probe. If the version is below the minimum, it responds with a backend_version_below_minimum warning, naming the version, the minimum, and what exactly will break. Nothing is disabled: an old version gives loud failures on specific routes, not quiet empty answers.

Version

What disappears

Whom it affects

SHM < 2.18.0

GET /healthcheck — the only route without authorization

only platform_probe: shm.live stays null, “billing is down” and “wrong password” stop being distinguishable (shm_healthcheck_route_absent). Other tools are unaffected

SHM < 2.11.3

GET /admin/user/search

client_search, client_resolve — failure, not an empty list

SHM < 2.9.0

GET /user/referrals

client_account_state loses the referral counter

SHM < 2.4.0

GET /user/email

client_account_state loses the address and the confirmation flag

Panel < 3.0.0

user is addressed by uuid, not numeric id

client_overview, subscription_inspect, traffic_stats, provisioning_diagnose, subscription_ops: /api/users/{id} is rejected by validation with 400

Panel < 3.0.0

no /api/connections/*

connections_inspect — the whole tool

Panel < 3.0.0

no POST /api/users/{id}/actions/extend

subscription_ops loses renewal (the panel only has bulk)

Panel < 3.0.0

no /api/system/stats/digest and /stats/http

panel_activity loses two of its five samples

Panel < 3.2.0

no GET /api/system/configuration

only platform_probe: the remna.subscriptionRequestHistory capability stays unknown — deliberately, not false

Remnawave 3.x breaks compatibility with everything written for 2.x, and breaks it quietly. The uuid was removed from the user object, and with it the by-telegram-id, by-email, and by-tag routes disappeared, and /api/users/{uuid} responds 400, not 404 — so the failure doesn't even look like “no such user”. These routes are not present here at all; where the server does encounter a legacy uuid (for example, in an old SHM storage snapshot), it says so in the response rather than silently sliding into a guess.

OpenAPI specifications lag behind working systems, so platform_probe carries a specs_are_stale warning on every call; SHM is worse than usual — its spec stamps info.version from the config at runtime, meaning it describes the stand where the export was made, not yours. So the probe does not read versions from files at all, but asks the working systems — and there it also establishes what is true for this deployment: whether the server-side filter narrows anything in SHM, whether the panel respects filters in the user listing (both respond 200 and silently discard unknown parameters), whether the panel keeps a subscription request log, whether the realtime traffic route exists, which ssh tunnels are open. And it separates “backend is down” from “our credentials are wrong”: 401/403 is reported as credentialsRejected.

Installation

You need Node 22.12+ and pnpm, and at least one of the two systems — SHM or Remnawave. Both are not required: each is configured separately and alone is a full configuration. Tools for a system that is absent are not published at all — they don't “answer empty”, they are absent, and platform_probe directly names what is configured. So the number of tools depends on the installation: panel only — 16, SHM only — 18, both — 34 (and more in rw mode).

pnpm install
pnpm build
pnpm run setup

pnpm run setup, specifically with run. pnpm setup is a built-in command of pnpm itself: it edits your shell profile and never reaches this repository.

The wizard exists because the step it replaces — writing .env by hand — fails silently: a typo in the panel token does not prevent the server from starting and later surfaces as a tool error in the middle of an unrelated question. So it checks each credential against the working system and distinguishes three failures — the host did not respond at all (DNS, TLS, closed port), the host responded and rejected the credentials, the host responded with something that proves nothing (502, 429): they are fixed differently, and a single “login failed” would send you to fix the wrong thing.

It asks only about the system you have and about the access mode; everything else is tucked behind a single question Configure the optional settings? [y/N]. It reads the timezone from the working SHM rather than guessing: SHM writes dates in its own local time without an offset, and a wrong zone silently shifts every age. It does not print secrets. By default it sets ro; for rw it requires typing the word rw and a separate confirmation — first naming how many tools will appear and how many of them write to the real billing and the real panel, counted from the registry at that moment. It writes .env with 0600 permissions over a copy of the previous one, carrying over variables it did not ask about, and prints connection commands for Claude Code, Codex, and opencode — but it does not edit other people's configs: a wizard that rewrites JSONC would one day break someone's working setup. You can restart it at any time; Enter keeps the existing value. Without a terminal it refuses to start: an MCP client starts the server without a TTY, and a wizard capable of waking up there would hang on a question no one sees.

Or by hand

cp .env.example .env && chmod 600 .env    # и заполнить

Each variable is described in .env.example. A missing or incorrect one crashes the start with the variable name and what is expected from it, instead of surfacing later as an obscure tool error.

{
  "mcpServers": {
    "hq": {
      "command": "node",
      "args": ["/absolute/path/to/hq-mcp/apps/stdio/dist/index.js"]
    }
  }
}

Second transport: MCP over HTTP

The same set of tools is available over HTTP — this is needed when the client cannot start the process itself: it is in a container, on another machine, or there are several of them. A separate application, configuration from the same .env:

# метка произвольная (её показывает /metrics), токен — не короче 24 символов:
# openssl rand -hex 24
HQ_MCP_HTTP_TOKENS='<label>:<token>' pnpm --filter @hq/http start
# hq-mcp http ready: url=http://127.0.0.1:42480 mode=ro profile=human tools=34 …

Without HQ_MCP_HTTP_TOKENS it does not start at all, and it refuses before building clients to billing and panel. It listens on loopback; to open it to the network — HQ_MCP_HTTP_HOST=0.0.0.0, and a warning is printed about that, because only that token will remain between the server and the network. The port is HQ_MCP_HTTP_PORT. The client connects to /mcp, passing the token with the usual Authorization: Bearer:

{
  "mcpServers": {
    "hq": {
      "type": "http",
      "url": "http://127.0.0.1:42480/mcp",
      "headers": { "Authorization": "Bearer <тот же токен>" }
    }
  }
}

The route is sessionless: Mcp-Session-Id is neither issued nor required, so you can keep several copies of the process behind a reverse proxy without sticky connections. It has no server messages, so GET on the SSE stream and DELETE on session close respond 405 — an MCP client understands this. A request with an Origin header is rejected with 403: protection against DNS rebinding, see “Limitations”.

The neighboring /v1/tools is not MCP but an internal REST facade for ai-bot: one list handle and one per call, with its own response envelope and its own request ceiling.

Production HTTP image

A production image is built only from a verified 40-character lowercase commit SHA. This SHA is sealed simultaneously into an OCI label, a root-owned read-only file, and /healthz; the entrypoint restores the value from the file, so a runtime override of HQ_MCP_IMAGE_REVISION does not change the health evidence.

pnpm test && pnpm test:guards && pnpm typecheck && pnpm build
HQ_MCP_COMMIT_SHA="$(git rev-parse HEAD)"
test "${#HQ_MCP_COMMIT_SHA}" -eq 40
docker build --build-arg "HQ_MCP_DEPLOYMENT_REVISION=${HQ_MCP_COMMIT_SHA}" --tag "hq-mcp-http:${HQ_MCP_COMMIT_SHA}" .
scripts/http-container-smoke.sh "hq-mcp-http:${HQ_MCP_COMMIT_SHA}"

A Compose consumer pins exactly this 40-character tag and does not declare host ports. The container runs as UID/GID 10001, publishes only /healthz and the REST facade in bot+ro, and requires a shared non-secret HQ_MCP_DEPLOYMENT_CONFIG_REVISION in lowercase UUID format. Both revisions are included in health so that ai-bot can shut down before reading the catalog if they do not match.

Production passes secrets only through three regular non-symlink files with exact mode 0600: SHM_ADMIN_AUTH_FILE, REMNA_API_TOKEN_FILE, and HQ_MCP_HTTP_TOKENS_FILE. The latter contains only ai-bot:<dedicated token>. This is a separate server token; SHM and Remnawave credentials are also allocated to this deployment separately and are not reused from the support bot.

Fitting to your own installation

The upstream danuk/shm does not know the word "Remnawave" — not a single line. The bridge between billing and the panel lives entirely in your provisioning templates: one panel user per user_service_id, name <NAME_PREFIX><user_service_id>, configuration snapshot in SHM storage under <STORAGE_PREFIX><user_service_id>. The server reads both prefixes at runtime from config.remnawave of your SHM and allows overriding (HQ_MCP_STORAGE_PREFIX, HQ_MCP_PANEL_PREFIXES) — the operator knows what is in the panel today better than a configuration key describing what SHM will build tomorrow.

The panel username is the only linking key, and a prefix that matches nothing does not produce an error: it produces a confidently wrong answer in which every service looks unprovisioned. Therefore, tools that can prove this return code prefix_unverified and suppress the affected finding — sync_audit does not return the missingPanelUser bucket at all, provisioning_diagnose marks the result with the same code or panel_username_guessed. The convention is needed by exactly three tools (sync_audit, provisioning_diagnose, the storage_edit mutator); client_overview accepts remna_user_id as an optional parameter and without it simply does not show half of the panel. If you do not have the convention, all other tools work as usual, and these three do not invent findings. The full breakdown, with prefix order and inherited names, is in COMPATIBILITY.md.

Tools

Thirty-four are visible in ro; rw mode adds fifteen from the last table and removes nothing. The numbers are for the human profile; what bot sees of this is stated in the security model.

Platform and a single client

Tool

What it answers

platform_probe

What is alive right now: versions, capabilities, tunnels, and whether a failure is an outage or credentials

client_resolve

Any identifier (telegram id, email, login, id, panel name) into canonical ids of both systems — all matches, not the first

client_search

Search SHM clients by fragment, with server-side match count

client_overview

The client in full in both systems in one call

client_account_state

How the account signs in: email and its confirmation, OTP, passkey, whether password login is possible, referrals

client_billing_view

Money through the client's eyes: upcoming charge and the payment methods actually offered to them

client_catalog_view

Catalog and promo codes through one client's eyes — their discount, their bonuses, plans hidden from them

Money, catalog, configuration

Tool

What it answers

billing_ledger

Payments, bonuses, charges, and two independent reconciliations (balance and bonus are different columns with different update paths)

autopay_inspect

Autopay state and all withheld fees — it lives in the JSON comment field of payment rows, not in user.settings

promo_read

Promo codes and their redemptions: these are different rows and cannot be read together

catalog_read

Plans, order pricing, child services, event map, categories — the source of valid service_id values

config_read

One SHM configuration key from a closed list, secrets masked. There is no whole-configuration read

template_read

Template list or the body of exactly one — the file that actually produces a notification or provisioning script

Services and provisioning

Tool

What it answers

service_inspect

Client services: status, expiry, scheduled next plan, spool tasks per service

spool_inspect

Provisioning queue: stuck, failed, paused, and the real depth

provisioning_diagnose

"Paid but no config" — per service, not per client

sync_audit

Batch reconciliation of billing with the panel, both sides read to the end

notify_history

Whether the client was actually told, and if not — why; a delivery verdict that nothing else shows

server_inventory

SHM's own transports and their groups (ssh, http, mail, telegram) and gaps that silently stop provisioning. This is not a Remnawave node list

Panel — first from the client side, then from the fleet side

Tool

What it answers

subscription_inspect

Remnawave card: status, expiry, traffic, HWID devices, recent subscription requests. Keys — never

subpage_read

What the subscription page actually shows the client: platforms, apps, install steps, button links

client_reach

Which nodes this client can actually reach and which inbound squads and tags provide that

device_inventory

HWID picture across the whole fleet — the base without which one client's device count means nothing

traffic_stats

Daily traffic broken down by nodes and squads; this is a time series, not card counters

connections_inspect

Who is connected right now. The panel answers this with a job, and the tool polls it itself

infra_map

Nodes × configuration profiles × inbounds × hosts × squads and the gaps between them

infra_costs

What the infrastructure costs, at the junction with the panel: a paid node nobody reaches is money going out

country_health

Nodes, online status, traffic, and hosts of one country

node_config_audit

What a profile declares versus what the panel would actually hand to a node

squads_read

Both squad families: internal ones decide access, external ones decide how the subscription is presented

panel_activity

What is happening with the panel itself: summary, digest over a window, which routes are being hit, subscription request history

torrent_reports

Torrent-blocker evidence — and, separately, whether it is installed at all and whether it is watching

Behind the tunnel (these two fail without it, naming the exact ssh command)

Tool

What it answers

abuse_report

Anti-abuse hook findings plus panel tops. Expensive: unlimited scans of the working billing MySQL, cap of 5 calls per 5 minutes

sql_query

Read-only SQL only — preflight and nothing more, see below

Writing (only rw, only human profile, plan first)

Tool

What it changes

billing_adjust

Balance or bonuses of an SHM client

billing_refund_service

Returns to the balance the amount SHM recorded as charged for the current paid period

bulk_ops

Bulk operations on panel clients — by a named set of ids or across the entire fleet

host_edit

One Remnawave host: label, address, port, SNI/host/path/ALPN/fingerprint, security layer, tags, enable and hide

host_cleanup

Deletes hosts by an explicit list of uuids. Irreversible

node_manage

One node: enable, disable, restart, reset_traffic, update, create

subscription_ops

One subscription in the panel: enable, disable, extend, reset_traffic, revoke, set_limits, device removal

service_lifecycle

Client service: give, touch, change_plan, schedule_change, stop, activate, delete

provisioning_repair

retry, resume or pause of one stuck spool task

template_edit

Overwrites the body of an existing SHM template

storage_edit

Writes a custom SHM storage by the list of keys printed for this installation

server_edit

An SHM transport line or transport group — webhooks, ssh provisioning endpoint, mail senders

user_flags

Blocks a client or edits safe card fields (full_name, phone, comment)

ops_confirm

Applies a plan by its plan_id. Writes what the scheduled tool writes

ops_audit

Nothing. Reads the local mutation log — rw because the log is part of the mutation surface

Mutations

Nothing is applied by the call that asks for it. A mutator without a plan_id reads the current state, builds the target state, and returns a plan: before, after, a field-level diff, side effects, rollback where one exists, and an identifier. It writes nothing. Applying is a second call:

ops_confirm { "plan_id": "…" }          # либо: тот же мутатор, ТЕ ЖЕ аргументы, плюс plan_id

A plan is bound to the profile that built it, to the tool it was built for, and to the hash of the arguments: it cannot be redeemed by a different caller, by a different tool, or by the same tool with a single changed number. It lives for 10 minutes. Single-use is an atomic rename on disk, not "read and delete": out of twenty simultaneous confirmations exactly one wins, the rest get "not found". A failed plan is not burned by a refusal — all checks happen after acquisition, and a failed one puts the file back; the attempt itself burns it, and if the backend crashed, the plan is spent. This is intentional, and it is the difference between one charge and three. Before applying, the tool re-reads the world and compares it with the snapshot the plan was built from: if the object has moved, the plan is rejected rather than rolled on top of someone else's change.

Every attempt is logged to HQ_MCP_AUDIT_PATH (JSONL, permissions 0600): who, with what, with which arguments, what the object looked like before and after, and how it ended — planned, applying, applied, failed or rejected; refusals on par with successes. applying is written before the backend is touched, and that is the whole point of the construction: a record without a paired terminal one means the process died mid-way, the plan snapshot is already destroyed, and money may have moved. ops_audit searches for such unclosed records across the entire log, regardless of the requested window, and reports them first; unparsed lines are counted, not silently skipped.

The framework holds the ceilings, not the tool author. A mutation above HQ_MCP_MAX_OP_AMOUNT is rejected before the plan is built, and the framework refuses to register a tool that declared a money endpoint but did not say how to read the amount from its input. The ceiling covers both kinds of money movement, and the second is easy to miss: both payments and bonuses, where the caller names the amount, and lifecycle actions that spend the client's balance (give, touch, change_plan, activate), where the amount is the plan price from the catalog. A plan whose price could not be read is not issued: not knowing the number does not make the charge free. HQ_MCP_MAX_BULK_USERS limits how many panel clients one bulk operation may touch, and a plan that could not establish that number from the panel is rejected rather than estimated by eye. Above the ceiling the operation is rejected in full — never truncated.

The ceiling is checked when the plan is built and only there: applying works off an already-built plan and does not measure it again. The ceiling cannot be bypassed this way — the arguments are pinned by the hash — but a ceiling lowered in .env after the plan was issued will not affect that plan.

template_edit and storage_edit first write their own rollback to HQ_MCP_BACKUP_DIR (directory 0700, files 0600); the path is returned in the response, restore_from puts the bytes back, and neither writes without a snapshot taken. The backup is deliberately separate from the plan snapshot: template bodies and configuration snapshots carry secrets as bare substrings with no field name to mask them by — so they cannot ride back to the model inside before/rollback; and the rollback must survive a change, whereas plan snapshots are swept within an hour.

Two more things the writing tools refuse to do. A body with <redacted:…> markers is never written: it is the output of a reading tool, and writing it would replace a live credential with the very word used to hide it. And raw panel blobs (finalMask, xhttpExtraParams, muxParams, sockoptParams) are excluded from any host patch — in a working installation a noticeable share of hosts carry a working Hysteria2 password inside finalMask.

What is actually proven, and what is not

host_edit is the only mutator whose apply branch was run against a live system: on a working Remnawave 3.2.3 panel we changed a host label, verified that the password in finalMask survived and that nothing beyond the declared field changed, and rolled it back. Everything else is proven up to and including the plan: the plan is built on real data, the applying part is covered by tests, but its branch was not run against a live system. This should be read literally. A plan that looks correct is a certificate about a plan.

Security model

Two profiles. human is a trusted operator, and gets concrete actionable refusals, including the exact ssh command when the tunnel is down. bot is an untrusted channel: any refusal collapses into the same message, so the registry cannot be probed to feel out which names answer differently. No writing tool is ever offered to a bot: in rw the bot profile sees the same twenty reading tools as in ro.

A forbidden class, separate from merely dangerous. These operations are not gated — they do not exist, and a build-time scanner fails the run if their path appears in the source as a literal. Identity and node keygen routes (GET, whose response body carries the private key). Token, authorization and passkey routes (the panel returns tokens in plaintext, and a created token is a permanent admin past all gates). Panel and subscription settings. The full /admin/config export. Manually marking a provisioning task successful — it does not do the work, it only moves the service to ACTIVE while the user is still absent from the panel. Deleting a payment, bonus or charge — a bare DELETE FROM on the registry, with users.balance not recalculated. Ready-to-use subscription links and connection-keys. restart-all, reorder, squad bulk-actions and PUT /admin/spool with job_users — broadcast to all clients with no undo.

The class narrowed, and every narrowing was a fix, not a relaxation. Reading templates was forbidden along with writing, although the reason — no git, no rollback — spoke only about writing; the breadth cost was not theoretical: a noticeable share of notifications in one observed window rendered empty and sent nothing, the task reported SUCCESS, and the cause of the silence lies inside the template body. Now reading is open, POST is under template_edit, which brought a rollback with it, and PUT and DELETE are closed: a template that just appeared and one that just disappeared have no previous state to capture. The ban on /api/sub was prefix-based and also covered /api/subscription-page-configs and /api/subscription-request-history — two reading controllers that issue no keys; now it is exact plus a prefix on /api/sub/. Bulk operations on panel clients were forbidden because they apply to the whole database with no list to review — true exactly until someone counts: bulk_ops counts at the panel before applying, refuses when the number could not be established or is above HQ_MCP_MAX_BULK_USERS, and is not offered to a bot.

POST /api/users/bulk/delete-by-status remains forbidden by its very shape: its body carries a status, not a list of people. The panel queues the task and deletes whoever matches at the moment it runs — not the people the operator reviewed — and answers 202 with an empty body and no counter, so accounts that expired in the meantime are deleted invisibly. The capability is preserved as bulk_ops delete_by_status: it enumerates specific ids, shows them, and deletes exactly those via bulk/delete. Bulk routes on other entities — hosts, nodes, squads, spool broadcasts — have no such counting step and remain absent.

Secrets are masked on output — both by key name and by value shape. By name: a closed list of credential keys, a match against token|secret|key|password|auth with an explicit exception list, tail masking for several, and PII masking for the bot profile. That was not enough, and in one day it failed three times: a Telegram bot token rode inside response.request.url of a spool line (the key is called url), the same token sat in the host column of SHM transport lines, and template bodies carry credentials as bare substrings with no field name next to them at all. So the pass-through also runs every passing line through value-shape rules: JWT; NAME=<value> where the name promises a secret and the value does not look like a placeholder; Telegram bot tokens with and without surrounding path; user:password@ inside URLs. It lives inside redact, which both HTTP clients call on input and the executor calls on output — no individual tool has to remember it.

The rules are calibrated, not guessed, and the calibration is declared right in the source: the "opaque run" threshold (32+ characters that look random) was measured on real template bodies and disabled on structured API responses, where data-URI icons and the hex uniq_id of a payment cross it — cutting those out would have extinguished exactly the fields the tool was written for. This is still not a security boundary, and the source says so: a secret written in words has no shape; everything that passes the filter stays inside the human profile. The same rules are used by scripts/no-secrets.test.ts, which keeps a secret out of a published commit: two copies of the knowledge of "what a secret looks like" diverge silently, and the second one keeps looking functional.

sql_query does not execute anything. It validates and refuses, and says so in its own source. Lexical checking is a cheap first filter and clearly not a security boundary; the module lists the bypasses that pass through it, and the tests keep them open so that no one mistakes the filter for a guarantee. Until execution is wired up, the preconditions are declared in the same file: read-only role, read-only transaction, query timeout, and a forbidden column list.

What to know about the limitations

  • The HTTP transport speaks MCP (/mcp, streamable HTTP) and exposes the same set of tools as stdio: one function publishes them for both transports. What it deliberately cannot do: sessions (Mcp-Session-Id is not issued), server-initiated messages, and with them — an SSE stream on GET and resumption via Last-Event-ID. Each call is self-contained, so the server and transport are created fresh per request; the SDK itself requires this, as its sessionless transport must not be reused.

  • On the /mcp route, two executor outcomes are unreachable, and the /metrics counters see two out of four for it. Invalid input is parsed by the SDK BEFORE the tool, and it answers -32602 itself; a nonexistent name is also rejected by it, without reaching the registry. Therefore invalid_input and not_found do not appear on this route either in the response or in the report. On the REST facade, both are reachable.

  • A request to /mcp with an Origin header is rejected with 403, no exceptions: the server listens on the loopback, and a browser page can point its own domain at 127.0.0.1 and go there on behalf of the operator. The browser sets Origin on any cross-origin POST; a real MCP client never does, and the server sends no CORS headers, so it has no browser client and cannot have one. The built-in allowedHosts/allowedOrigins are not suitable for this: in this SDK version they are marked deprecated in favor of external middleware, and an empty origin list for them means "checking is disabled," not "no origin is acceptable."

  • Two tools need a tunnel into the internal network, and without it they refuse. They remain visible deliberately: a vanished tool teaches the model that such a capability does not exist, when in fact the port is closed.

  • sync_audit reads out both systems to the end and is the only expensive call here — for that it has its own request quota.

  • The panel page size is measured at runtime, not assumed: the API does not declare a maximum, and the actual one changed between releases.

  • The panel's bulk routes respond with 202 or 204 and an empty body and queue part of the work, so "applied" means "accepted by the panel," not "done for everyone." The number set by the plan in advance is the only honest one that exists here at all.

  • Edits made in the panel are not carried back into SHM billing, and the tools that make them say so. There is no reconciliation step; sync_audit will show you the discrepancy later.

Development

pnpm test          # модульные тесты
pnpm typecheck
pnpm test:guards   # сканер секретов и предохранители скрипта захвата фикстур

Tests run on fixtures that mirror the shape of real responses. Where a defect was visible only on a live system, the test that pins it down says so.

License

MIT.

Maintenance

ActivityMaintained
ResponsivenessSyncing

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    A read-only MCP server for securely accessing Xendit payment platform data. It enables querying balances, invoices, transactions, disbursements, refunds, and virtual account payments while preventing any money-moving operations.
    13
    MIT
  • A
    license
    B
    quality
    C
    maintenance
    MCP server for the DataGate billing platform API, providing read-only tools to manage customers, invoices, products, agreements, sites, and payments.
    13
    MIT

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/qwertyhq/hq-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server