Skip to main content
Glama
README.md
# MCP Ops Gateway

An MCP server that:
1. **Exposes** RAG search + ServiceNow + Observability as tools to any MCP client.
2. **Acts as an MCP client itself**, proxying calls to remote ServiceNow and
   Observability MCP servers.

Built on **standalone FastMCP 3.x** (PrefectHQ), not the `mcp` v2 beta bundled
class — as of mid-2026, the standalone package is the stable, production
choice for composing/proxying multiple MCP servers.

## 1. Install

```bash
python -m venv .venv && source .venv/bin/activate
pip install -e .
cp .env.example .env   # fill in real values
```

## 2. Run locally (stdio, for Claude Desktop)

```bash
python server.py
```

Add to Claude Desktop config (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "ops-gateway": {
      "command": "/absolute/path/to/.venv/bin/python",
      "args": ["/absolute/path/to/mcp-gateway/server.py"],
      "env": {
        "SERVICENOW_INSTANCE_URL": "...",
        "SERVICENOW_MCP_URL": "...",
        "SERVICENOW_CLIENT_ID": "...",
        "SERVICENOW_CLIENT_SECRET": "...",
        "SERVICENOW_TOKEN_URL": "...",
        "OBSERVABILITY_MCP_URL": "...",
        "OBSERVABILITY_API_KEY": "..."
      }
    }
  }
}
```

## 3. Run in production (Streamable HTTP)

```bash
export MCP_GATEWAY_TRANSPORT=streamable-http
export MCP_GATEWAY_AUTH_TOKEN=$(openssl rand -hex 32)
python server.py
```

Or via Docker:

```bash
docker build -t mcp-ops-gateway .
docker run -p 8080:8080 --env-file .env mcp-ops-gateway
```

Clients then connect to `http://your-host:8080/mcp` with
`Authorization: Bearer <MCP_GATEWAY_AUTH_TOKEN>`.

## 4. Debug with MCP Inspector

```bash
npx @modelcontextprotocol/inspector http://localhost:8080/mcp
```

## Architecture notes / production decisions

- **Curated proxying, not blind forwarding.** `tools/servicenow_tools.py`
  and `tools/observability_tools.py` don't just relay every remote tool.
  Each tool is explicitly defined with its own Pydantic-validated schema,
  docstring (which becomes the tool description the LLM sees), and error
  handling. This is deliberate: exposing a remote server's full raw tool
  surface to an agent is both a security risk (e.g. accidental `delete_record`
  exposure) and a UX problem (vague, remote-server-authored descriptions
  make for worse agent tool selection).

- **Auth is mandatory over HTTP transport.** `server.py` refuses to start
  with `streamable-http` transport unless `MCP_GATEWAY_AUTH_TOKEN` is set.
  For real production, replace `StaticTokenVerifier` with an OAuth 2.1 /
  JWT verifier tied to your IdP (Okta/Azure AD) — FastMCP supports pluggable
  `AuthProvider`s for this.

- **Remote calls get retry + backoff** (`clients/base_client.py`, via
  `tenacity`), because remote MCP servers over a network WILL occasionally
  time out or drop connections. Only transport-level errors are retried —
  a 4xx/validation error from the remote tool is NOT retried.

- **Short-lived connections per call.** Each `call_tool` opens and closes
  its own connection to the remote server. This is simplest and safest for
  low/medium QPS. If you're calling a remote server dozens of times/sec,
  switch to a connection pool / persistent `Client` held in an
  `contextlib.AsyncExitStack` at server startup instead — see FastMCP docs
  on `Client` lifecycle for the pattern.

- **ServiceNow OAuth token caching** (`clients/servicenow_client.py`).
  ServiceNow's REST/MCP layer expects OAuth2 client-credentials tokens, not
  static API keys. The token is cached in-process and refreshed 60s before
  expiry rather than fetched on every call.

- **RAG embedding model is baked into the Docker image** at build time
  (see `Dockerfile`) so pod cold-starts don't depend on reaching
  HuggingFace at runtime — important if your network egress is locked down
  (as is typical in BFSI environments).

- **`mask_error_details`**: consider setting this `True` on `FastMCP(...)`
  in production so internal exception text (which might leak infra details)
  isn't sent verbatim to the LLM/client. Currently tool-level code catches
  and summarizes errors instead, which is the safer default.

## Extending

To add a new remote MCP server (e.g. a CMDB or a paging system):
1. Add a client in `clients/<name>_client.py` following the ServiceNow or
   Observability pattern (whichever auth style matches).
2. Add curated tool wrappers in `tools/<name>_tools.py`.
3. Register in `server.py`: `<name>_tools.register(mcp)`.

## Known remote-server assumption

This code assumes your ServiceNow and Observability *remote* MCP servers
expose generic tools like `get_record`/`create_record`/`update_record` and
`query_timeseries`/`search_logs`/`list_active_monitors` respectively. Adjust
the `tool_name` strings and argument shapes in `tools/servicenow_tools.py`
and `tools/observability_tools.py` to match whatever remote MCP servers
you're actually pointing at — run `list_tools()` via `RemoteMCPClient`
against your real remote server first to see its actual tool names/schemas.