# Connect an MCP client to BMJ Content Gateway

The Content Gateway exposes a read-only, customer-scoped MCP endpoint at:

```text
https://dev.bmj-content-hub.com/mcp
```

Use the `workers.dev` hostname for every machine client. The
`content-gateway.bmj-cloudflare-hackathon.com` hostname sits behind the zone's
bot protection, which serves a browser challenge to MCP clients, SDKs and
`curl`; it is for humans reading the documentation and the admin console. That
protection is zone-wide on the current plan and cannot be switched off for
the API hostname alone, so a dedicated API hostname on its own zone is the
planned replacement for `workers.dev`.

It implements the stateless MCP `2026-07-28` protocol. Every request is
self-contained: clients do not need to initialize a session, and the server
does not return `Mcp-Session-Id`.

This is a beta service using BMJ's open corpus. It is not a clinical system and
its generated or retrieved content is not medical advice.

## What you need

- A Content Gateway customer key beginning `bmjp_`.
- A client that supports MCP `2026-07-28`, or a currently supported legacy
  Streamable HTTP client.
- A customer contract that includes corpus metadata and a content grant.

Store the key in a secret such as `BMJ_EVIDENCE_API_KEY`. Send it only in the
`Authorization` header on every request:

```http
Authorization: Bearer bmjp_...
```

Never put the key in a URL, MCP prompt, tool argument, log or source file.

## Recommended Python client

Install the typed client and its optional MCP transport from `clients/python`:

```bash
pip install ".[mcp]"
```

Then connect with the same key used by the Content Gateway REST API:

```python
import asyncio
import os

from bmj_content_gateway import BMJMCPClient


async def main() -> None:
    async with BMJMCPClient(
        "https://dev.bmj-content-hub.com",
        api_key=os.environ["BMJ_EVIDENCE_API_KEY"],
    ) as bmj:
        tools = await bmj.list_tools()
        print([tool["name"] for tool in tools])

        collections = await bmj.list_collections()
        print(collections.total)


asyncio.run(main())
```

The compatibility transport may perform an older `initialize` exchange, but
the server still keeps no session. New protocol implementations should use the
request format below.

## Discover the modern endpoint

`server/discover` is the optional preflight for a modern client:

```bash
curl -X POST 'https://dev.bmj-content-hub.com/mcp' \
  -H "Authorization: Bearer ${BMJ_EVIDENCE_API_KEY}" \
  -H 'Accept: application/json, text/event-stream' \
  -H 'Content-Type: application/json' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: server/discover' \
  --data '{
    "jsonrpc": "2.0",
    "id": "discover-1",
    "method": "server/discover",
    "params": {
      "_meta": {
        "io.modelcontextprotocol/protocolVersion": "2026-07-28",
        "io.modelcontextprotocol/clientInfo": {
          "name": "BMJ example client",
          "version": "1.0.0"
        },
        "io.modelcontextprotocol/clientCapabilities": {}
      }
    }
  }'
```

Use the same `_meta` fields and `MCP-Protocol-Version` on every modern request.
`Mcp-Method` must equal the JSON-RPC method. For `tools/call`,
`resources/read`, and `prompts/get`, also send `Mcp-Name`. It must equal
`params.name` for a tool or prompt and `params.uri` for a resource.

## List and call tools

List the tools by changing the method and routing header to `tools/list`.
The following calls the public trust-marker registry through the authenticated
MCP boundary:

```bash
curl -X POST 'https://dev.bmj-content-hub.com/mcp' \
  -H "Authorization: Bearer ${BMJ_EVIDENCE_API_KEY}" \
  -H 'Accept: application/json, text/event-stream' \
  -H 'Content-Type: application/json' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: tools/call' \
  -H 'Mcp-Name: list_trust_markers' \
  --data '{
    "jsonrpc": "2.0",
    "id": "tool-1",
    "method": "tools/call",
    "params": {
      "name": "list_trust_markers",
      "arguments": {},
      "_meta": {
        "io.modelcontextprotocol/protocolVersion": "2026-07-28",
        "io.modelcontextprotocol/clientInfo": {
          "name": "BMJ example client",
          "version": "1.0.0"
        },
        "io.modelcontextprotocol/clientCapabilities": {}
      }
    }
  }'
```

The endpoint exposes these read-only tools:

| Tool | Purpose |
|---|---|
| `list_articles` | Browse customer-scoped article metadata |
| `get_article` | Read one article and its effective trust profile |
| `list_collections` | List published BMJ collections in the customer scope |
| `get_collection` | Read one collection definition and resolved count |
| `list_collection_articles` | Resolve customer-visible members of a collection |
| `get_article_trust` | Read effective article trust markers |
| `list_trust_markers` | Read the trust-marker vocabulary |
| `find_articles` (in this release) | Article-level full-text search over the D1 FTS index |
| `find_passages` (in this release) | Paragraph-level full-text search over the D1 FTS index |

Every tool call is metered on the same customer quota as the REST API and
recorded in the request audit as operation `mcp_tool`. A quota denial comes
back as a tool-call error with `code: quota_denied` inside the JSON-RPC
result, not as an HTTP-level rejection — check `structuredContent.ok` and
`structuredContent.code` rather than only the HTTP status.

## Access model

A tool call succeeds only when the package and active agreement permit MCP,
the key selects MCP, the package and key both permit the tool's underlying
operation, and the active content release has a sufficient grant level
(`metadata` < `preview` < `chunk`). The key's low-level scope is derived from
its selected operations and checked again as defence in depth. The catalogue
and trust tools above need
`retrieve:metadata` (plan), `customer:read` (key scope) and a `metadata`
grant; `find_articles`/`find_passages` additionally need `search:lexical`
(plan) and `search:evidence` (key scope). `GET /v1/customer/context` reports
the same check as an `access` array — one row per mode with
`{ capability, scope, grant, plan, key, grant_ok, effective }`; its `api_key`
object also reports `operation_ids`, `transports` and `mcp_tools` — so a client
can call it once and know which tools will actually work before calling
`tools/list`.

All nine tools apply the same operation, key, content-grant and collection
checks as the REST routes (see the developer documentation's access matrix).
The catalogue/trust tools (`list_articles`, `get_article`,
`list_collections`, `get_collection`, `list_collection_articles`,
`get_article_trust`, `list_trust_markers`) all require `retrieve:metadata` on
the plan, `customer:read` on the key, and a `metadata` content grant.
`find_articles` and `find_passages` require `search:lexical` on the plan and
`search:evidence` on the key, with the same `metadata` grant. Call
`GET /v1/customer/context` before your client attempts a tool to read its
`access` array and confirm the row's `effective` field is `true`, rather than
discovering `insufficient_scope` or `entitlement_denied` from a live call.

Resources include `bmj://collections` (only when Browse is selected) and
`bmj://schemas/article-trust-profile/v1`. `tools/list` exposes only the tools
selected for the presenting API key and backed by one of its selected
operations. A key issued before the tool selection existed sees every tool
its agreement, scopes and content grant would allow it to call, and nothing
more: discovery always matches execution. Collection and article results are always
intersected with the agreement's snapshotted collections and current content
release. A key can narrow that agreement but never expand it.

## Responses and caching

Send `Accept: application/json, text/event-stream`. The server can return a
complete JSON response or request-scoped server-sent events. A response stream
belongs only to that POST and is not a resumable session.

Complete discovery, list and resource results can include `ttlMs` and
`cacheScope`. Respect `private` cache scope: never reuse an authenticated entry
for another key or customer. Tool calls that read changing article data should
be treated as current request results unless they provide an explicit hint.

## Browser clients

Server-side clients normally send no `Origin` header. A browser client is
accepted only from the Content Gateway origin or an exact origin configured by
an administrator in `MCP_ALLOWED_ORIGINS`. Ask the service owner to add the
full origin, including scheme and port where applicable. Wildcard and suffix
matching are not supported.

## Common errors

| Status/code | Meaning | Action |
|---|---|---|
| HTTP 401 | Key is missing, invalid or revoked | Supply the current key in `Authorization`; do not retry an invalid key indefinitely |
| HTTP 403 `insufficient_scope` | The key lacks `customer:read` | Ask an administrator to issue the correct customer key |
| HTTP 403 `entitlement_denied` | The active contract has no metadata right or content grant | Ask the account administrator to review the customer contract |
| HTTP 403 `invalid_origin` | Browser origin is not configured | Use a server client or request an exact origin allow-list entry |
| HTTP 400 / `-32020` | Routing headers disagree with the JSON-RPC body | Make method, name and protocol fields identical |
| HTTP 400 / `-32022` | Protocol version is unsupported | Use one of the versions returned in the error; prefer `2026-07-28` |
| HTTP 405 | This transport method is not available | Send one JSON-RPC message with POST |

## Authentication roadmap

The beta endpoint uses an issued BMJ API key, not an interactive OAuth login.
An MCP host that requires OAuth discovery cannot yet connect directly. The
planned production design uses the MCP OAuth 2.1 profile, short-lived tokens
bound to this resource, and the same server-side BMJ customer contract checks.

## Further reading

- [Content Gateway developer documentation](https://dev.bmj-content-hub.com/docs)
- [MCP 2026-07-28 release](https://blog.modelcontextprotocol.io/posts/2026-07-28/)
- [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http)
- [MCP authorization](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization)
