Files
shopdb-flask/mcp/shopdb_mcp.py
cproudlock f0b5465917 mcp: read-only ShopDB MCP server generated from the OpenAPI spec
A separate tool (not shipped in the app) that exposes a curated set of read
endpoints as MCP tools, so an LLM client can query the asset DB directly. Built
with FastMCP.from_openapi over docs/openapi.json; auth via a scoped PAT
(SHOPDB_TOKEN) or managed X-API-Key. Read-only: only GETs on the curated
allowlist become tools, all writes excluded. Runs anywhere that can reach the
API - never on the air-gapped box. Needs `pip install fastmcp` + testing in that
env (not installed in this repo's venv).
2026-07-30 07:52:53 -04:00

80 lines
2.8 KiB
Python

"""ShopDB MCP server (read-only v1).
Exposes a curated set of ShopDB *read* endpoints as MCP tools so an LLM client
(Claude Desktop / Claude Code / an agent) can query the asset database directly.
Tools are generated from docs/openapi.json - keep that spec current
(scripts/gen_openapi.py) and this server tracks it.
This is a SEPARATE tool from the shopdb app: it runs wherever you can reach the
API over HTTPS and calls it with a scoped Personal Access Token. It never needs
to run on the (air-gapped) prod box.
Safety:
- Read-only. Only GET endpoints in the CURATED set below become tools; every
write (POST/PUT/PATCH/DELETE) and everything off the allowlist is excluded.
- Auth is a scoped PAT you supply. Give it the least scope needed (read).
- The app audits API calls (AuditLog), so tool activity is traceable.
Run:
pip install -r mcp/requirements.txt
export SHOPDB_BASE="https://tsgwp00525.wjs.geaerospace.net/shopdb"
export SHOPDB_TOKEN="<scoped read PAT>" # Bearer
# or: export SHOPDB_API_KEY="<managed X-API-Key>"
python mcp/shopdb_mcp.py # stdio transport
Widen later (add write tools) by extending CURATED / adding RouteMaps for the
verbs you want, behind a token that actually has those scopes.
"""
import json
import os
import httpx
from fastmcp import FastMCP
from fastmcp.server.openapi import MCPType, RouteMap
_HERE = os.path.dirname(os.path.abspath(__file__))
_SPEC = os.path.join(_HERE, '..', 'docs', 'openapi.json')
BASE = os.environ.get('SHOPDB_BASE',
'https://tsgwp00525.wjs.geaerospace.net/shopdb')
TOKEN = os.environ.get('SHOPDB_TOKEN')
API_KEY = os.environ.get('SHOPDB_API_KEY')
# High-value read surfaces only - keeps the tool count small so the model picks
# well. Add more prefixes here as needed.
CURATED = (r'^/api/(search|assets|reports|printers|computers|machines|network|'
r'measuringtools|applications|knowledgebase|locations|vendors|'
r'businessunits|dashboard)(/|$)')
def build_server():
if not (TOKEN or API_KEY):
raise SystemExit('Set SHOPDB_TOKEN (Bearer PAT) or SHOPDB_API_KEY.')
headers = {}
if TOKEN:
headers['Authorization'] = 'Bearer ' + TOKEN
if API_KEY:
headers['X-API-Key'] = API_KEY
client = httpx.AsyncClient(base_url=BASE, headers=headers, timeout=30.0)
spec = json.load(open(_SPEC))
route_maps = [
# curated read endpoints -> tools
RouteMap(methods=['GET'], pattern=CURATED, mcp_type=MCPType.TOOL),
# everything else (writes + off-list reads) -> not exposed
RouteMap(mcp_type=MCPType.EXCLUDE),
]
return FastMCP.from_openapi(
openapi_spec=spec,
client=client,
name='ShopDB (read-only)',
route_maps=route_maps,
)
if __name__ == '__main__':
build_server().run()