The JWT's exp claim says 30 days, and I took that as the session
lifetime. It is only an outer ceiling. The server also keeps a per-token
whitelist entry in Valkey (jwt:{accountId}:{jti}) whose TTL is
JWT_TIMEOUT_SECONDS — 7200s on this instance — and JwtStrategy.validate
re-sets it on every authenticated request. Two hours idle and the token
is rejected with 29 days still on exp.
Proven, not inferred: the token from yesterday returned 401 at 13.8h old.
The live instance publishes the values unauthenticated at
GET /api/v3/config/public — JWT_TIMEOUT_SECONDS 7200,
JWT_SHOW_TIMEOUT_WARNING_SECONDS 3600, the latter being exactly the
one-hour UI prompt that prompted this investigation.
refresh-session turns out not to be special: it extends through the same
guard as any other route, and uniquely only in returning the remaining
TTL. So the keepalive uses GET /api/v3/me instead, and the server stays
GET-only; the one POST in the repo is in scripts/probe.mjs, where it
reports the idle budget.
JWT_EXTENDED_TIMEOUT_SECONDS (~1 month) exists in the config schema but
is vestigial: privateDevice has no references in the current NestJS
source, and generateJwtAndAddToWhitelist never overrides the TTL.
Also fixes a real breakage this surfaced: TypeScript parameter
properties are rejected by Node's type stripping, so `npm run dev` and
`npm test` both failed on any file reaching them. Rewritten as explicit
fields, and noted in CLAUDE.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
33 lines
1.2 KiB
JavaScript
33 lines
1.2 KiB
JavaScript
#!/usr/bin/env node
|
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
import { loadConfig } from '../config.ts';
|
|
import { SessionKeepalive } from '../keepalive.ts';
|
|
import { SchulcloudClient } from '../schulcloud/client.ts';
|
|
import { createServer } from '../server.ts';
|
|
|
|
/**
|
|
* stdio entry point — for running the server locally against Claude Code or
|
|
* Claude Desktop. The remote deployment uses bin/http.ts instead.
|
|
*
|
|
* Nothing may be written to stdout here except MCP protocol frames.
|
|
*/
|
|
async function main(): Promise<void> {
|
|
const config = loadConfig();
|
|
const { server } = createServer(config);
|
|
await server.connect(new StdioServerTransport());
|
|
|
|
// A desktop client left open overnight idles far past the instance's 2h
|
|
// session timeout, so stdio needs the keepalive just as much as HTTP does.
|
|
// It logs to stderr; stdout carries protocol frames only.
|
|
if (config.keepaliveIntervalMs > 0) {
|
|
new SessionKeepalive(new SchulcloudClient(config), config.keepaliveIntervalMs).start();
|
|
}
|
|
|
|
console.error(`[schulcloud-mcp] stdio transport ready for ${config.baseUrl}`);
|
|
}
|
|
|
|
main().catch((error: unknown) => {
|
|
console.error('[schulcloud-mcp] fatal:', error);
|
|
process.exit(1);
|
|
});
|