MCPDBWizard

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:

  1. The URL, ending in the config name exactly as it appears on the Runtime page.
  2. 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.
  3. 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:

ResponseMeaningFix
401 with WWW-Authenticate: BearerNo token, or one that is bad, revoked or unknownRe-issue the token on the Accounts tab
403The token is valid; that account has no grant for this configTick it on the Access grid
503Granted, but nothing is running for that configStart it on the Runtime page
429 with Retry-AfterPer-caller rate limitWait, 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:

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

SymptomUsual cause
Client connects, but lists no toolsRight server, wrong config — or a config whose objects were all curated away
Works from curl, not from the clientThe client is not sending the header, or cannot do Streamable HTTP
403 on a config you can see in the consoleYou are an ordinary user without a grant; admins are permitted implicitly, so it works for them
Everything 503s after a restartThe servers do not start themselves unless the config is set to run on start
Intermittent 429 under light loadThe burst is too small for the initialize handshake; its floor is 5 for exactly this reason