Documentation · Operating
Connecting to the MCP server
A running config is reached at:
http://<host>:8080/mcp/<config>
with an API token in an Authorization: Bearer header. That is the whole connection: one URL,
one header, the transport is Streamable HTTP.
Connect to the proxy, not to the generated server
The generated servers listen on 8090–8109, and it is tempting to point a client straight at one.
Don’t — and the reason is not tidiness.
A generated server cannot tell two callers apart. It connects to Oracle as one shared account
and sees a single bearer token, which is a door key rather than an identity. Everything that depends
on who is asking — the accounts, the access matrix, per-caller rate limits, the MCP-ACCESS
record naming a user — exists only in the proxy on 8080. Publish a generated port and you have not
bypassed a formality, you have given up the entire notion of a caller.
That is also why the quickstart’s docker run publishes 8080 and never one of 8090-8109.
It does also publish 9464, but that is the Prometheus scrape port: it carries counters, not
tools, so nothing about caller identity rests on it.
What a client needs
Three things, and the third catches people out:
- The URL, ending in the config name exactly as it appears on the Runtime page.
- An API token, issued on the Users → Accounts tab and shown exactly once — only a BCrypt hash is stored, so a lost token is replaced, never recovered.
- A grant. A token authenticates; it does not authorise. A new account starts with no configs, so tick the config on the Users → Access grid. A token that works against one config and 403s against another is working correctly.
Configuring a client
Clients that speak Streamable HTTP take a URL and headers:
{
"mcpServers": {
"payroll": {
"url": "http://localhost:8080/mcp/payroll",
"headers": { "Authorization": "Bearer <id>.<secret>" }
}
}
}
The token is two parts joined by a dot — an id and a secret. Paste it whole.
Once connected, ask the agent what it can do. It will list the tools back, which is the quickest check that the config generated the surface you intended — and the quickest way to notice that it did not.
Checking it by hand
Before blaming a client, try the same thing with curl:
curl -X POST http://localhost:8080/mcp/payroll \
-H 'Authorization: Bearer <id>.<secret>' \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
A reply listing tools means the server, the token and the grant are all good, and anything still broken is on the client side.
A reply may come back as plain JSON or as an SSE frame, so a hand-rolled client has to handle
both. A real MCP client also spends initialize and notifications/initialized before it does any
work — worth knowing when you are reading logs and wondering why a client that has done nothing has
already made three requests.
What a refusal means
Every refusal is a JSON-RPC error object, never an HTML page:
| Response | Meaning | Fix |
|---|---|---|
401 with WWW-Authenticate: Bearer | No token, or one that is bad, revoked or unknown | Re-issue the token on the Accounts tab |
| 403 | The token is valid; that account has no grant for this config | Tick it on the Access grid |
| 503 | Granted, but nothing is running for that config | Start it on the Runtime page |
429 with Retry-After | Per-caller rate limit | Wait, or raise MCPDBWIZARD_MCP_PER_CALLER_RATE_LIMIT |
A config you have not been granted is refused identically whether or not it exists. That is deliberate: distinguishing the two would make the proxy a way to enumerate config names. So a 403 on a name you are certain of is worth checking for a typo — you will get the same answer either way.
Sessions
Mcp-Session-Id passes through in both directions. A GET opens the server’s SSE stream and is
meant to stay open — there is no request timeout on it, and in the access record its ms is the
life of the stream rather than a latency. DELETE ends the session.
A client that walks away mid-stream is the ordinary end of a subscription, not an error.
Two headers the proxy does not pass on
Both are deliberate, and both have surprised someone:
Authorizationis replaced. Your API token is consumed by the proxy and swapped for the target server’s own token, which the web app generated at start-up and never writes down. The generated server never sees your token, and no token you hold will work directly against8090.Originis dropped. The generated server validatesOriginagainst a loopback allowlist and allows an absent one. Forwarding a browser’s real origin would fail that check for no benefit.
If you must expose a generated port
Read the section above again first. If you still need it — usually to put a dedicated reverse proxy in front — note that the server refuses to start on a non-loopback address unless it was generated with bearer-token authentication. Exposing the port is the single act that puts a server on a network while both of its protections are opt-in.
TLS does not satisfy that guard. It encrypts the wire and restricts nobody, so TLS without a token is an open server that is merely hard to eavesdrop on.
Troubleshooting
| Symptom | Usual cause |
|---|---|
| Client connects, but lists no tools | Right server, wrong config — or a config whose objects were all curated away |
Works from curl, not from the client | The client is not sending the header, or cannot do Streamable HTTP |
| 403 on a config you can see in the console | You are an ordinary user without a grant; admins are permitted implicitly, so it works for them |
| Everything 503s after a restart | The servers do not start themselves unless the config is set to run on start |
| Intermittent 429 under light load | The burst is too small for the initialize handshake; its floor is 5 for exactly this reason |
Related
- Creating application users — accounts, tokens and the access matrix
- Docker parameters — every variable, and which need a regeneration
- Setting up auditing — the
MCP-ACCESSrecord naming who called