ReNile-MCP
ReNile-MCP
A remote MCP server that exposes the ReNile IoT platform over Streamable HTTP. Clients connect by URL and sign in with their own ReNile account through OAuth; each user sees only their own devices.
Tools
Tool | Purpose |
| Lists every device ( |
| Latest sensor readings. Omit |
Each reading carries sensor, value, unit, lower_limit, upper_limit,
status (normal/high/low/unknown), timestamp, age_seconds, and a
derived is_stale flag.
is_stale matters: the platform serves last-known values indefinitely, so a
reading can report status: normal while being over a year old. Anything older
than STALE_AFTER_SECONDS (default 1 hour) is flagged so it is not mistaken for
a current condition.
An unmatched or ambiguous device returns matched: false plus the valid device
names, rather than failing — so the model can correct itself in the same turn.
How authentication works
The server is an OAuth 2.1 resource server. It issues nothing and stores no credentials:
A client connects to
/mcpwith no token and gets401plusWWW-Authenticate: Bearer ..., resource_metadata="...".It follows that to
/.well-known/oauth-protected-resource/mcp, which names the ReNile authorization server, and runs the OAuth flow there.It retries with the user's access token. The server forwards that token verbatim to the ReNile API.
The server does not validate tokens — no signature check, no audience check.
The ReNile API is the sole authority and answers 401 if a token is bad. The
token's sub, iss and exp claims are read (unverified), for two reasons
documented in src/core/auth.py: to bind an MCP session to one principal, so a
session id is not a usable credential for a different user; and to reject an
expired token at the door, where the client still gets a re-auth challenge.
Opaque (non-JWT) tokens work too — the session is bound to a hash of the token instead — but expiry can then only be discovered upstream.
What the ReNile backend must provide
/.well-known/oauth-authorization-server(RFC 8414)/authorizeand/token:authorization_codewith PKCES256, plusrefresh_tokenDynamic Client Registration (RFC 7591) — without it, every client product must be pre-registered by hand
The
resourceparameter (RFC 8707), so tokens are audience-bound/api/v1/devices/names/and/api/v1/snapshot/accepting those access tokens, and scoped to the authenticated user401for an expired token, kept distinct from403for a permission failure
Setup
uv sync
cp .env.example src/.env # then set the two required URLs
chmod 600 src/.env
uv run uvicorn src.app:app --host 0.0.0.0 --port 8000Variable | Default | |
| — | Required. The ReNile authorization server. No trailing slash. |
| — | Required. The exact public URL clients use, including |
| (empty) | Comma-separated. Must include the public hostname or requests are rejected with 421. Each entry also matches that host on any port. |
| (empty) | Comma-separated. |
|
| |
|
| See "Scaling" below. |
|
| Scheme used when forwarding the token upstream. The platform uses |
|
| |
|
| |
|
| |
|
| |
|
| Total attempts per request; |
|
| Shared upstream pool size. |
|
| How much of an upstream error body to quote back. |
|
| |
|
| Age past which a reading is flagged |
Deployment
docker build -t renile-mcp .
docker run -p 8000:8000 --env-file deploy.env renile-mcpRoutes: /mcp, /.well-known/oauth-protected-resource/mcp, and /healthz
(unauthenticated, liveness only — it makes no upstream call because it has no
token to make one with).
Serve it over HTTPS. The access token is a bearer credential on every
request. Terminate TLS at a proxy and keep --proxy-headers on.
Two settings break the first deploy if they are wrong:
RENILE_RESOURCE_SERVER_URLmust byte-match the URL clients actually use. It is what the 401 challenge points at; get it wrong and OAuth discovery dead-ends silently.ALLOWED_HOSTSmust contain the public hostname. DNS-rebinding protection is always on, and an unlisted host is answered with421 Invalid Host header. A bare hostname also matches that host on any port, solocalhostcoverslocalhost:8000.
Scaling
Sessions are held in a per-process dict, so stateful serving needs one worker per
process and sticky routing at the load balancer. For several replicas behind a
round-robin balancer, set STATELESS_HTTP=true — but note that this also removes
the session-ownership check, which only matters for older clients (2025-era
protocol versions); newer ones are per-request either way.
Connecting a client
Add https://mcp.renile-iot.com/mcp as a remote MCP server (Claude, ChatGPT and
Cursor all take a URL) and complete the ReNile sign-in when prompted.
Development
uv run pytest # unit + HTTP protocol tests
uv run uvicorn src.app:app --reload # local run
curl -i localhost:8000/mcp # expect 401 + WWW-AuthenticateThe test suite drives the real ASGI app in-process — no uvicorn subprocess, no
port to race on. tests/test_http.py covers the assertions that are easy to
break silently: the 401 challenge carries resource_metadata, the caller's token
reaches the upstream API verbatim, two concurrent callers never cross tokens, and
one user's session id is refused to another.
Layout
src/app.py ASGI entrypoint: transport security, uvicorn target
src/server.py MCP wiring: tools, lifespan, auth settings, errors
src/core/auth.py the pass-through token verifier
src/core/config.py pydantic-settings; the only reader of the environment
src/core/logging.py stderr logging
src/services/processing.py payload shaping: matching, staleness, responses
src/services/renile_client.py async httpx client; the token is a per-call argumentLatest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/mohamedelsayadrenile/IOT-MCP'
If you have feedback or need assistance with the MCP directory API, please join our Discord server