diff --git a/mcp/README.md b/mcp/README.md new file mode 100644 index 0000000..e258095 --- /dev/null +++ b/mcp/README.md @@ -0,0 +1,42 @@ +# ShopDB MCP server (read-only) + +Lets an LLM client query ShopDB directly as tools, generated from the OpenAPI +spec (`docs/openapi.json`). Separate from the app - runs anywhere that can reach +the API; never on the air-gapped prod box. + +## Install + run +``` +pip install -r mcp/requirements.txt +export SHOPDB_BASE="https://tsgwp00525.wjs.geaerospace.net/shopdb" +export SHOPDB_TOKEN="" # mint in ShopDB: Settings > API Tokens +python mcp/shopdb_mcp.py # stdio +``` +(Or `SHOPDB_API_KEY` instead of a Bearer PAT for managed-token access.) + +## Claude Desktop +Add to `claude_desktop_config.json`: +```json +{ + "mcpServers": { + "shopdb": { + "command": "python", + "args": ["/abs/path/shopdb-flask/mcp/shopdb_mcp.py"], + "env": { + "SHOPDB_BASE": "https://tsgwp00525.wjs.geaerospace.net/shopdb", + "SHOPDB_TOKEN": "" + } + } + } +} +``` + +## Scope / safety +- **Read-only**: only GET endpoints on the curated allowlist (`CURATED` in + `shopdb_mcp.py`) become tools; all writes are excluded. +- Give the PAT the **least** scope needed. Tool calls hit the normal API, so + they're subject to its auth + land in the audit log. +- To add write tools later, extend `CURATED` / add `RouteMap`s for those verbs, + behind a token that actually holds the scopes. + +## Keeping it current +Tools follow `docs/openapi.json`. After API changes: `python scripts/gen_openapi.py`. diff --git a/mcp/requirements.txt b/mcp/requirements.txt new file mode 100644 index 0000000..b89f032 --- /dev/null +++ b/mcp/requirements.txt @@ -0,0 +1,2 @@ +fastmcp>=2.9 +httpx>=0.27 diff --git a/mcp/shopdb_mcp.py b/mcp/shopdb_mcp.py new file mode 100644 index 0000000..43d0631 --- /dev/null +++ b/mcp/shopdb_mcp.py @@ -0,0 +1,79 @@ +"""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="" # Bearer + # or: export SHOPDB_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()