MCP: AI clients
Smart Parks Protect exposes a Model Context Protocol server so that Claude, ChatGPT and other MCP clients can answer questions about a project's entities, devices, positions, measurements, events and processing traces, and act within the AI action policy (architecture section 27). Every request an AI client makes runs as the user who connected it, within the permissions of that user and the scopes the user granted, and lands in the audit log.
How it fits together
| Part | Where | Role |
|---|---|---|
| MCP server | services/mcp, container protect-mcp, URL <PUBLIC_URL>/mcp |
Streamable HTTP endpoint with tools, resources and prompts. Verifies access tokens and calls the API with them. Never touches the database. |
| Authorization server | The API, /.well-known/oauth-authorization-server and /api/v1/oauth/* |
OAuth 2.1 with PKCE, client registration by metadata document or dynamic registration, consent, token issue and refresh. |
| Consent and connections | The web application, /oauth/consent and /account/connections |
The user approves a client and can disconnect it later. |
The MCP server is a resource server in OAuth terms: it publishes protected resource metadata at /.well-known/oauth-protected-resource (and the path-suffixed variant), answers 401 with a WWW-Authenticate challenge that points there, and requires every read scope on every request.
Connecting a client
The server must be reachable from the client's network over HTTPS (Claude and ChatGPT connect from their own infrastructure, see the deployment guide). The MCP URL is the public URL of the server plus /mcp, for example https://protect.example.org/mcp.
- Claude (web, desktop, mobile, Cowork). Customize, Connectors, Add custom connector, paste the URL. Leave the client id and secret empty. Claude identifies itself with its client id metadata document; the consent page shows
claude.aias the client. - Claude Code.
claude mcp add --transport http protect https://protect.example.org/mcp, then/mcpto sign in. Claude Code redirects to a loopback port on your machine; the consent page warns about that. - ChatGPT. Settings, Connectors (developer mode), add the URL. ChatGPT identifies itself with its client id metadata document. Deep research uses the
searchandfetchtools. - MCP inspector.
npx @modelcontextprotocol/inspector, connect tohttp://localhost:8001/mcpon a development machine. The inspector registers dynamically and runs in a browser, so addhttp://localhost:6274toCORS_ORIGINS.
On the first connection the client opens the consent page in a browser. Sign in, check the client and the scopes, and choose Allow. The client receives an access token that lives one hour and a refresh token that lives thirty days and is rotated on every use.
What a client can do
Tools, all read-only and bounded:
| Tool | Reads |
|---|---|
list_projects |
The user's projects and roles; the starting point |
search_entities |
Entities of a project by name, type or status (at most 100) |
get_entity |
One entity with its type, current device, assignment history and latest position |
get_device |
One device with type, assignments, external identities and deep links |
get_latest_position |
The newest position of an entity or device |
query_measurements |
Aggregated measurements per metric and entity (the Data Explorer's series endpoint, at most five metrics per call) |
list_metrics |
Metric keys with data in the project |
query_events |
Events with alert status, newest first (at most 100) |
get_processing_trace |
One processing trace with its steps and error |
search_traces |
Traces of a project's devices, filtered by device, identity, status or error code |
search |
The generic search ChatGPT expects: entities and devices by name |
fetch |
The generic fetch ChatGPT expects: any smartparks:// record |
Resources: smartparks://projects/{id}, smartparks://projects/{id}/entities/{id}, smartparks://projects/{id}/events/{id}, smartparks://devices/{id}, smartparks://traces/{id}. Entities and events carry the project in the URI because the API is project scoped.
Prompts: analyze_device_health and investigate_missing_data guide a client through the tools for the two questions rangers ask most.
Write and action tools
A client can also act, within the AI action policy (decision D87, ADR 0019):
| Tool | Does | Scope |
|---|---|---|
create_event |
A manual event (sighting, note, incident) with type, title, optional entity and location | events:write |
acknowledge_alert |
Closes an alert with a note | alerts:write |
request_device_status |
Queues a status request to a device | devices:control |
request_device_position |
Queues a position request to a device | devices:control |
confirm_action |
Executes a proposal the policy held for confirmation | the action's scope |
get_ai_policy |
The server's policy per action, so the client knows what to expect | none |
Every action goes through one API endpoint, POST /api/v1/mcp/actions, which checks the
token's scope, the person's project permission and the policy. The policy (Server admin, AI
clients policy) has a mode per action: allowed executes at once, confirmation (the
default) stores a proposal for ten minutes and the client must show the person the summary
and call confirm_action after they agree, privileged is the same with the note that a
privileged client should hold it, disabled refuses. High-impact control (configuration
downlinks, firmware) is disabled and not configurable in this version. Actions are audited
as the person through the client, and the results carry the MCP actor in traces and the
command lifecycle. Rule changes and configuration are not exposed.
Consent: the consent page lists the write scopes separately; a client that requests no scopes is offered all of them, and the person may untick the writes.
Authentication in detail
Scopes: projects:read, entities:read, devices:read, positions:read, measurements:read, events:read, traces:read, plus offline_access for a refresh token. The MCP server requires all read scopes; a token with fewer gets a 403 with insufficient_scope and the client asks the user to re-consent.
Access tokens are JWTs signed with the server's JWT_SECRET, with the MCP URL as audience, the user as subject, the client id and the scopes as claims, valid for JWT_LIFETIME_SECONDS. The API accepts them only for GET requests on the paths a scope covers (protect_api/oauth/scopes.py); any other request is refused with 403. A session token from the web application is never accepted by the MCP server, and an MCP token cannot do anything a session token can outside its scopes.
Client registration: the authorization server advertises client_id_metadata_document_supported and a registration endpoint. A client whose id is an HTTPS URL is fetched from that URL, must be self-referential, and may only redirect to its own host or a loopback address. Dynamically registered clients must redirect to HTTPS or loopback URIs. Loopback redirect URIs match with the port ignored (RFC 8252), which native clients need.
Every API request made through the MCP server writes an audit row (mcp.request) with the user, the client id, the tool name, the path and the response status. Server admins see them in the audit log filtered on actor mcp.
Disconnecting: Connected AI clients in the sidebar lists the clients a user has authorized; Disconnect revokes their refresh tokens. The running access token expires within the hour. A password change also invalidates every token issued before it.
Configuration
| Variable | Default | Meaning |
|---|---|---|
MCP_PUBLIC_URL |
PUBLIC_URL + /mcp |
The canonical MCP URL and token audience. Set only when the MCP server has its own host name. |
MCP_PORT |
8001 |
Host port of the container on a development machine |
API_INTERNAL_URL |
http://api:8000 in compose |
Where the MCP service reaches the API |
OAUTH_CONSENT_LIFETIME_SECONDS |
600 |
How long a consent request stays valid |
OAUTH_CODE_LIFETIME_SECONDS |
300 |
Lifetime of an authorization code |
OAUTH_REFRESH_TOKEN_LIFETIME_DAYS |
30 |
Lifetime of a refresh token |
nginx routes /mcp and /.well-known/oauth-protected-resource to the MCP service and /.well-known/oauth-authorization-server to the API, with a rate limit of 300 requests per minute per address on /mcp.
Limits
- Tool results are bounded by row limits and time windows; ask for narrower windows rather than more rows. Large extracts belong in an export job, which a later release exposes as a tool.
- Claude accepts tool results up to about 150,000 characters and waits at most five minutes per call.
- The MCP server is stateless: any replica can answer any request, and no session state is kept between calls.
Verification status
Verified with Claude on 2026-09-04 against the dev server: Claude registered through its client id metadata document, the user approved once on the consent page, and the questions "Which projects do I have", "What is the latest position of Rhino 14" and "Why has SP05-demo stopped updating" were answered through the tools, every call recorded in the audit log as the user, read-only. ChatGPT is pending: connecting a custom MCP server there needs Developer mode, which OpenAI offers on Pro, Business, Enterprise and Education accounts. The server meets ChatGPT's documented requirements, including the search and fetch tools, and the check takes an hour once an account with Developer mode exists.