Skip to main content
Glama
markberin

Open iT / ServiceNow MCP Pilot

by markberin
README.md
# Open iT / ServiceNow MCP Pilot

This repository contains a deliberately small remote MCP server scaffold for an Open iT / ServiceNow SAM Pro pilot. It exposes read-only tools over Streamable HTTP. Product inventory is deterministic mock data; Knowledge Base searches use the public documentation at `docs.openit.com`. The server does **not** call Open iT product or ServiceNow APIs.

## What it exposes

- MCP endpoint: `/api/mcp`
- Public health endpoint: `/api/health`
- Tool: `find_products_without_entitlements`
- Tool: `summarize_entitlement_coverage`
- Tool: `search_openit_knowledge_base`

The MCP endpoint follows the Microsoft Learn MCP pattern: clients use Streamable HTTP, while a normal browser `GET` receives HTTP `405 Method Not Allowed` and a short explanation. The endpoint is stateless and returns JSON MCP responses, which keeps it suitable for horizontally scaled or serverless hosting later. `api/index.py` exposes the same ASGI application as a future Vercel entry point; no deployment is included.

## Run locally

Python 3.11 or newer is required.

```powershell
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -e ".[dev]"
$env:OPENIT_MCP_API_KEY = "local-dev-key"
$env:MCP_BIND_HOST = "127.0.0.1"
$env:PORT = "8000"
$env:MCP_BASE_URL = "http://127.0.0.1:8000"
openit-mcp
```

For bash/zsh, activate with `source .venv/bin/activate` and configure the key with:

```bash
export OPENIT_MCP_API_KEY="local-dev-key"
export MCP_BIND_HOST="127.0.0.1"
export PORT="8000"
export MCP_BASE_URL="http://127.0.0.1:8000"
```

`MCP_BIND_HOST` and `PORT` are used only by the local `openit-mcp` Uvicorn launcher. `MCP_BASE_URL` is used only by external clients such as `examples/invoke.py`; the server itself routes internally with relative paths and never calls itself through an absolute URL.

The server intentionally returns `503` from MCP `POST` and `DELETE` requests when `OPENIT_MCP_API_KEY` is unset. Clients should send the key as a bearer token:

```text
Authorization: Bearer local-dev-key
```

For ServiceNow compatibility, the server also accepts the raw key in `Authorization` as well as `X-API-Key: local-dev-key`. The health endpoint does not require authentication.

Check health:

```powershell
Invoke-RestMethod "$env:MCP_BASE_URL/api/health"
```

In a second terminal with the same virtual environment and API key, run the included MCP client example:

```powershell
python examples/invoke.py
```

You can also connect an MCP Inspector or other Streamable HTTP client to `$MCP_BASE_URL/api/mcp` and configure the `Authorization` header shown above.

Opening the MCP URL directly in a browser returns:

```text
HTTP 405 Method Not Allowed
This is an MCP server endpoint and cannot be accessed directly via a browser. Please use a streamable HTTP MCP client.
```

## Tool contract

`find_products_without_entitlements` accepts:

| Input | Type | Required | Behavior |
| --- | --- | --- | --- |
| `publisher` | string | No | Exact, case-insensitive publisher filter |
| `limit` | integer | No | Defaults to `20`; allowed range is `1` to `100` |

It returns structured JSON in this shape:

```json
{
  "count": 1,
  "items": [
    {
      "product_name": "Open iT Analyzer Pro",
      "publisher": "Open iT",
      "reason": "No entitlement record found"
    }
  ],
  "explanation": "These products currently do not have matching entitlement records in the mock dataset.",
  "generated_at": "2026-08-05T12:00:00Z"
}
```

Results are sorted by publisher and product name before `limit` is applied.

### `summarize_entitlement_coverage`

This tool summarizes entitlement coverage across the mock product inventory. It accepts:

| Input | Type | Required | Behavior |
| --- | --- | --- | --- |
| `publisher` | string | No | Exact, case-insensitive publisher filter |

It returns structured JSON in this shape:

```json
{
  "publisher": null,
  "total_products": 3,
  "covered_products": 1,
  "uncovered_products": 2,
  "total_entitlement_records": 25,
  "coverage_percentage": 33.33,
  "explanation": "Coverage is the percentage of discovered products in scope that have at least one matching entitlement record in the mock dataset.",
  "generated_at": "2026-08-18T12:00:00Z"
}
```

Coverage is calculated at the product level: a product is covered when its entitlement count is greater than zero. An empty publisher result reports zero products and `0.0` percent coverage.

### `search_openit_knowledge_base`

This tool searches the live [Open iT Online Docs](https://docs.openit.com/) index and returns official page or section references. It accepts:

| Input | Type | Required | Behavior |
| --- | --- | --- | --- |
| `query` | string | Yes | Question or keywords; 3 to 200 characters |
| `limit` | integer | No | Defaults to `5`; allowed range is `1` to `10` |
| `include_summaries` | boolean | No | Defaults to `true`; includes a relevant description or indexed excerpt |

Each result contains the page or section title, documentation breadcrumb, optional summary, and direct `reference_url`. An agent should cite those URLs when it uses the returned guidance. Searches require outbound HTTPS access to `docs.openit.com`. The published index is cached in memory for 15 minutes to reduce repeated downloads while still refreshing as the online documentation changes.

## Tests

```powershell
pytest
```

The tests verify the published tool list, structured output, entitlement coverage summaries, documentation-index result parsing and references, publisher filtering, deterministic limiting, browser `405` behavior, real HTTP MCP initialization/list/call requests, the public health endpoint, and fail-closed API key behavior.

## Future hosted configuration

Loopback addresses are local-only: inside Vercel they refer to the function instance, not the public deployment or another service. The network-base address ending in `.0` at the start of the IPv4 loopback block is not an application host and must not be used. Hosted URLs must come from deployment environment variables, while same-app routes should remain relative.

Vercel imports the ASGI `app` from `api/index.py`; it does not execute the local Uvicorn launcher, so `MCP_BIND_HOST` and `PORT` are not deployment assumptions. Configure these server environment variables before hosting:

```text
OPENIT_MCP_API_KEY=<production-secret>
# Recommended when using a stable custom domain:
OPENIT_MCP_ALLOWED_HOSTS=openit-mcp.example.com,openit-mcp.example.com:*
```

The server automatically adds `VERCEL_URL`, `VERCEL_BRANCH_URL`, and `VERCEL_PROJECT_PRODUCTION_URL` to its MCP host allowlist when Vercel exposes those system variables. An explicit `OPENIT_MCP_ALLOWED_HOSTS` remains recommended for a stable custom domain.

Set `MCP_BASE_URL=https://openit-mcp.example.com` only if a hosted client or job runs `examples/invoke.py`; the MCP server routes `/api/mcp` and `/api/health` without it. Keep the API key in Vercel's secret environment configuration and use HTTPS. Environment changes apply only to new deployments, so redeploy after changing them.

## Connect from ServiceNow AI Agent Studio

Use the following values after deploying to Vercel:

| Field | Value |
| --- | --- |
| Name | `Open iT SAM Pilot` |
| Authentication type | `API Key` |
| MCP server URL | `https://<your-stable-vercel-host>/api/mcp` |
| API key | The raw value of `OPENIT_MCP_API_KEY` |

Do not add a `Bearer` prefix in the ServiceNow API Key field. The server accepts ServiceNow's raw `Authorization` value. A browser request to the URL still returns the intentional `405`; tool discovery uses authenticated MCP `POST` requests.

## Before ServiceNow integration

This pilot still needs a real Open iT data adapter, agreement on the product/entitlement matching rules and response fields, ServiceNow remote MCP connection configuration, production authentication (preferably OAuth 2.1 rather than a shared key), HTTPS hosting, logging/monitoring, secret rotation, and end-to-end validation from AI Agent Studio.