feat: MCP-Server ueber denselben Token wie die API

Sechs Werkzeuge: Aufgaben suchen, Woche ansehen, Kalender auflisten,
Aufgabe anlegen, abhaken und einplanen. Welche davon ein Modell zu sehen
bekommt, haengt an den Rechten des Tokens — was nicht erlaubt ist, taucht
gar nicht erst in der Liste auf. Ein Sprachmodell bekommt keinen eigenen,
weicheren Zugang.

Bewusst wenige und grob geschnittene Werkzeuge. Ein Modell, das zwischen
zwanzig fast gleichen waehlen muss, waehlt falsch; und jedes weitere ist
eine zusaetzliche Moeglichkeit, versehentlich etwas zu aendern.

Zustandslos: Jede Anfrage steht fuer sich. Der Planer hat keine
langlaufenden Vorgaenge, und eine Sitzungsverwaltung waere Zustand, den
mehrere Container nicht teilen koennten.

Jede schreibende Nutzung steht im Protokoll. Ein Modell, das Aufgaben
anlegt, muss genauso nachvollziehbar sein wie ein Mensch — eher mehr.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-13 14:31:19 +02:00
co-authored by Claude Opus 5
parent 5f45dff1a7
commit bdc4260d59
3 changed files with 545 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'
import { tokenAusKopfzeilen } from '@/lib/api'
import { tokenPruefen } from '@/server/api/token'
import { serverBauen } from '@/server/mcp/werkzeuge'
export const dynamic = 'force-dynamic'
/**
* Der MCP-Endpunkt.
*
* Ausgewiesen wird sich mit demselben Token wie bei `/api/v1` — ein
* Sprachmodell bekommt keinen eigenen, weicheren Zugang. Welche Werkzeuge es
* überhaupt zu sehen bekommt, hängt an den Rechten des Tokens: Was nicht
* erlaubt ist, taucht gar nicht erst in der Liste auf.
*
* Ohne Sitzungen: Jede Anfrage steht für sich. Der Planer hat keine
* langlaufenden Vorgänge, und eine Sitzungsverwaltung wäre Zustand, den
* mehrere Container nicht teilen könnten.
*/
async function bedienen(anfrage: Request): Promise<Response> {
const aufrufer = await tokenPruefen(
tokenAusKopfzeilen(
anfrage.headers.get('Authorization'),
anfrage.headers.get('X-Planer-Token'),
),
)
if (!aufrufer) {
return Response.json(
{
jsonrpc: '2.0',
error: { code: -32001, message: 'Kein gültiger Token.' },
id: null,
},
{ status: 401, headers: { 'WWW-Authenticate': 'Bearer' } },
)
}
const server = serverBauen(aufrufer)
const transport = new WebStandardStreamableHTTPServerTransport({
// Zustandslos: kein sessionIdGenerator.
enableJsonResponse: true,
})
await server.connect(transport)
try {
return await transport.handleRequest(anfrage)
} finally {
// Jede Anfrage bekommt einen frischen Server; sonst blieben Verbindungen
// offen, die niemand mehr schliesst.
await transport.close()
await server.close()
}
}
export async function POST(anfrage: Request) {
return bedienen(anfrage)
}
export async function GET(anfrage: Request) {
return bedienen(anfrage)
}
export async function DELETE(anfrage: Request) {
return bedienen(anfrage)
}