LANIS exposes one provider-neutral Streamable HTTP MCP endpoint at /mcp.
Claude connects through a custom connector. ChatGPT’s public plugin is planned;
operators can test the endpoint through its custom MCP connection flow where available. Every authorization creates an independent connection, including when
the same provider connects twice. No model is run by this feature.
- Open Settings → Agents for connection instructions and the MCP address.
- Add a custom connector in Claude or a remote MCP server in another client. Operators can test ChatGPT through its custom MCP flow where available. Use an OAuth-capable client to connect to the MCP server in another OAuth-capable client. Start connecting from that client.
- The client discovers OAuth metadata, registers its callback, and opens LANIS.
- LANIS keeps the authorization request through sign-in, shows the account, self-declared client name, callback domain, and requested permissions.
- Choose Restricted (the default), Full access, or Custom, then connect or deny. Individual permissions appear only under Custom. The screen uses the existing themed LANIS icon and shows the requesting domain.
- Return to the agent. Manage, reduce permissions, rename, or disconnect each connection in Settings → Agents. Access can be changed within the client’s original requested capability ceiling. Reconnect for capabilities the client did not request.
The app name is not verified identity. The consent screen explicitly explains this and displays the callback domain. Creating a client named “ChatGPT” does not make that client trusted. Public clients use dynamic registration and S256 PKCE; no school password or LANIS session token is given to the external client.
Set on the API:
LANIS_AGENT_ISSUER=https://YOUR_PUBLIC_API_ORIGIN
LANIS_AGENT_UI_ORIGIN=https://YOUR_LANIS_UI_ORIGINThese examples are configuration placeholders, not real endpoints. The issuer
must be an origin without a path, credentials, query, or fragment. Use HTTPS
outside local development. Local defaults are http://localhost:8000 and
http://localhost:5173. The UI's existing VITE_API_URL must point at the same API.
The approval URL includes the originating issuer. When the browser is configured
for a different backend, the UI blocks consent requests and offers the existing
backend-selection flow. It does not silently change backends or send existing
credentials to the issuer in the URL. The switch health-checks the chosen server,
clears the previous login and returns to the same request for a fresh sign-in.
Backend changes in another tab reload approval; stale agent API clients reject
requests until reload. A backend URL in a query parameter is never trusted as an
API destination without the explicit selection step.
Use the same persistent JWT_SECRET / credential encryption configuration as the
existing API. Install requirements.txt (including the official MCP Python SDK).
The existing startup migration adds agent tables to the existing auth database. Back up that database before deployment. Connections and hashed tokens persist across API restarts. There is no separate agent database or process to restart.
The reverse proxy must route /mcp, /oauth/*, /agents*, and /.well-known/*
to this API, retain Authorization and MCP headers, support POST/GET/DELETE, and
avoid caching these routes. Do not add an interactive proxy login in front of
MCP discovery. The UI must serve its SPA on /agents/authorize. The issuer's
host must reach the API unchanged (or be explicitly forwarded as the public
Host) for MCP DNS-rebinding protection. Configure trusted forwarded headers for
the actual proxy; never trust arbitrary forwarded IPs. Rate limits use the
server-validated client IP, so a proxy misconfiguration can pool clients together.
Public metadata endpoints:
/.well-known/oauth-protected-resource/mcp(also available at the root alias)/.well-known/oauth-authorization-server
The server requires OAuth on MCP, advertises code + refresh grants, accepts
public clients (token_endpoint_auth_method=none), requires S256 PKCE and state,
and checks exact registered redirects. HTTP callbacks are allowed only on
loopback. Tokens are opaque, hashed at rest, client-bound and MCP-only; normal
API session tokens cannot authenticate MCP and agent tokens cannot authenticate
normal API routes. Access tokens last at most one hour. Refresh tokens rotate,
with an absolute 90-day family lifetime. Code/refresh replay revokes the family.
Revocation and scope reductions are checked again for each tool call.
Pending browser requests expire after ten minutes; one-use codes after two minutes. Public registration and authorization are rate-limited per network identity; token requests have separate per-client and higher per-IP abuse limits. School reads have separate limits. Used refresh tokens remain until expiry for replay detection. Expired transient records are pruned during new authorizations/registrations. Revoked connection metadata is retained in account export and removed on account deletion; raw tokens, passwords, and callback state are excluded from exports. Requests that have not yet been approved have no user association and expire automatically.
Clients request the complete scope catalog by default. This is the ceiling, not an automatic grant. The server derives Restricted/Full scopes itself; it does not trust a preset's submitted checkbox list. Custom grants exactly the selected requested scopes. A client that explicitly requests a smaller scope set cannot receive more than that set; Full is disabled until the full catalog is requested.
- Restricted: ordinary read access, excluding grades, attendance, writes, and opening new message conversations (Schulportal marks those as read).
- Full access: all supported school-account reads and writes, including grades/attendance. No admin, credentials, account deletion, or grant-management tools are exposed to agents.
- Custom: choose each capability separately, including sensitive reads, message read-state changes, sending, homework, timetable overrides, submissions, preferences and binding elections.
| Capability | Scopes | Tools |
|---|---|---|
| Connection and clock | Always | lanis_connection |
| Schoolwork | schoolwork:read, schoolwork:write |
Courses, paginated homework/lessons, study groups; change homework completion |
| Timetable | timetable:read, timetable:write |
Raw plan and substitutions; recurring weekly personal lesson overrides |
| Calendar | calendar:read |
Events for an explicit year |
| Messages | messages:read, messages:mark-read, messages:write |
Headers and cached content; open/mark read; recipients, send and reply |
| Files | files:read |
Browse/search Dateispeicher, read files up to 2 MiB, list distributed files |
| Submissions | submissions:read, submissions:write |
List/detail, upload files up to 2 MiB, delete submitted files |
| Elections | elections:read, elections:write |
Discover forms/options; submit confirmed selections |
| Modules | profile:read |
Available school modules |
| Personal profile | profile:personal:read |
Sensitive school profile, identity and class data; Full or explicit Custom access |
| Sensitive records | grades:read, attendance:read |
Course marks and attendance (separate opt-ins) |
| Preferences | preferences:read, preferences:write |
Read/patch personal display preferences |
Personal profile access requires the new profile:personal:read scope. Existing
connections with profile:read retain module discovery only and must reconnect
and approve Full or explicitly select the personal profile permission in Custom
to read personal profile data. Restricted access excludes this permission.
Schoolwork responses always exclude grades and attendance, even under Full;
these have their own tools and grants. Timetable reads do not use homework
enrichment. Lesson override dates are weekday anchors, so edits and hiding recur
every week. Agents must obtain an explicit request for a recurring change;
one-off edits are not supported. lanis_message only reads the existing conversation cache; a cache
miss returns instructions instead of making an upstream request. The separately
permitted lanis_open_message discloses its read-state effect and has
readOnlyHint=false. Restricted therefore cannot silently clear unread reminders.
Discovery lists only tools allowed by the current grant. Direct calls also check the required scope. Mutating tools have write annotations; file deletion and binding elections are marked destructive. Agents are instructed to perform writes only on user request and avoid automatic retries of ambiguous sends, uploads and submissions. Election submission requires an explicit confirm flag. External content is untrusted data, never instructions. IDs must come from read tools, dates use Europe/Berlin, and school outages are not empty results.
OAuth consent uses the middleware's existing shared lifecycle lease; it never upgrades it to an exclusive lease. Agent code exchange and refresh check account existence through their existing SQLite transaction, without invoking session cleanup on a second connection. Tests cover real signed browser sessions and an expired session belonging to another account.
The LANIS UI keeps ChatGPT marked Coming Soon until the supported public
plugin is available. For operator/private testing, use ChatGPT’s Add custom
MCP server flow with the deployed https://YOUR_PUBLIC_API_ORIGIN/mcp URL
and OAuth, where enabled for the account/workspace. Complete LANIS consent.
See OpenAI’s connection guide.
Remote MCP publication uses the portal’s With MCP path. The ZIP includes
plugin.json, mcp.json and the existing LANIS branding. Do not select the
Skills only upload path for this integration. Public uploads must not contain
private app bindings or credentials. See the publishing preparation guide
for the exact remaining inputs, review walkthrough and deployment gate.
After the API and UI are deployed, build against the verified public issuer:
python scripts/build_agent_plugin.py --issuer https://YOUR_PUBLIC_API_ORIGIN --output /tmp/lanis-plugin.zipThe builder verifies live OAuth discovery, dynamic registration and scope shapes before writing the archive. It does not upload, publish or deploy anything. A successful build is not a completed submission: publisher details, listing URLs, a working demo and reviewer access must also be ready.
Official references: ChatGPT MCP plugins, plugin authentication, Claude custom connectors, MCP authorization.
python -m pytest tests/test_agents.py tests/test_account_data.py tests/test_auth_identity.py tests/test_account_session_revocation.py -qThe tests use fake accounts in temporary SQLite databases. They exercise real OAuth HTTP endpoints and the real MCP transport, scope discovery, direct-call permission checks, token replay, expiry, account isolation/deletion, independent agents, and data projection. They do not contact Schulportal.
The UI demo at /demo/settings/agents has isolated sample connections for
interaction testing. It never creates a real authorization. Browser testing of
approval should also cover sign-in, reload, permission changes, rejection,
expired requests, small screens, dark mode, and return to the requesting client.