From 5f45dff1a78889b51704590e559e7bca1999481e Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sun, 13 Sep 2026 14:31:17 +0200 Subject: [PATCH 1/5] refactor: Einplanen aus der Serveraktion herausloesen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Oberflaeche und der MCP-Server sollen dieselbe Stelle benutzen. Zwei Umsetzungen desselben Vorgangs waeren zwei Gelegenheiten, die Regeln unterschiedlich hinzuschreiben — und der Kalenderschreibweg samt Schleifenschutz ist nichts, was man zweimal richtig hinbekommt. Der Kern liegt bewusst nicht in einer Datei mit 'use server': Dort wird jede exportierte Funktion zu einem Endpunkt, den der Browser aufrufen kann. Eine Funktion, die den Akteur als Parameter nimmt, waere dann ein offenes Tor. Co-Authored-By: Claude Opus 5 --- package-lock.json | 813 ++++++++++++++++++++++++++++- package.json | 1 + src/server/aktionen/zeitbloecke.ts | 110 +--- src/server/zeitblock/dienst.ts | 132 +++++ 4 files changed, 932 insertions(+), 124 deletions(-) create mode 100644 src/server/zeitblock/dienst.ts diff --git a/package-lock.json b/package-lock.json index ca23b0b..c891339 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@date-fns/tz": "1.5.0", "@dnd-kit/core": "6.3.1", "@dnd-kit/utilities": "3.2.2", + "@modelcontextprotocol/sdk": "^1.30.0", "@prisma/adapter-pg": "7.10.0", "@prisma/client": "7.10.0", "@radix-ui/react-avatar": "1.2.6", @@ -1422,6 +1423,18 @@ "integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==", "license": "MIT" }, + "node_modules/@hono/node-server": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", + "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -2093,6 +2106,68 @@ "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==", "license": "MIT" }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", @@ -5435,6 +5510,19 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", @@ -5484,6 +5572,45 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -5871,6 +5998,43 @@ "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", "license": "MIT" }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/brace-expansion": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", @@ -5937,6 +6101,15 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/c12": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", @@ -5989,7 +6162,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6003,7 +6175,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -6173,6 +6344,28 @@ "devOptional": true, "license": "MIT" }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -6180,11 +6373,45 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "devOptional": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -6558,6 +6785,15 @@ "node": ">=0.10" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -6630,7 +6866,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -6650,6 +6885,12 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/effect": { "version": "3.20.0", "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", @@ -6692,6 +6933,15 @@ "node": ">=14" } }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/enhanced-resolve": { "version": "5.24.5", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", @@ -6824,7 +7074,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6834,7 +7083,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6879,7 +7127,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -6990,6 +7237,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -7505,6 +7758,36 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -7515,6 +7798,68 @@ "node": ">=12.0.0" } }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz", + "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/exsolve": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", @@ -7556,7 +7901,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "devOptional": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -7617,7 +7961,6 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", - "devOptional": true, "funding": [ { "type": "github", @@ -7666,6 +8009,27 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-my-way": { "version": "9.7.0", "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.7.0.tgz", @@ -7752,6 +8116,24 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -7771,7 +8153,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7845,7 +8226,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -7886,7 +8266,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -7984,7 +8363,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8070,7 +8448,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8099,7 +8476,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -8125,6 +8501,15 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hono": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.7.tgz", + "integrity": "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/html-encoding-sniffer": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", @@ -8147,6 +8532,26 @@ "node": ">=16" } }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -8164,7 +8569,6 @@ "version": "0.7.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", - "devOptional": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -8245,6 +8649,24 @@ "node": ">=12" } }, + "node_modules/ip-address": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz", + "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -8538,6 +8960,12 @@ "dev": true, "license": "MIT" }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-property": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", @@ -8701,7 +9129,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "devOptional": true, "license": "ISC" }, "node_modules/iterator.prototype": { @@ -8849,6 +9276,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -9345,7 +9778,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -9358,6 +9790,31 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -9382,6 +9839,31 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -9494,6 +9976,35 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/next": { "version": "16.3.4", "resolved": "https://registry.npmjs.org/next/-/next-16.3.4.tgz", @@ -9672,7 +10183,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9682,7 +10192,6 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -9812,6 +10321,27 @@ "devOptional": true, "license": "MIT" }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -9907,6 +10437,15 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -9921,7 +10460,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -9934,6 +10472,16 @@ "dev": true, "license": "MIT" }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -10065,6 +10613,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-types": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.3.tgz", @@ -10417,6 +10974,19 @@ "devOptional": true, "license": "ISC" }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -10462,6 +11032,22 @@ "node": ">=16.0.0" } }, + "node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -10483,6 +11069,34 @@ ], "license": "MIT" }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/rc9": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.1.0.tgz", @@ -10670,7 +11284,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10793,6 +11406,22 @@ "@rolldown/binding-win32-x64-msvc": "1.2.8" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -10950,12 +11579,57 @@ "semver": "bin/semver.js" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/seq-queue": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", "devOptional": true }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/server-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", @@ -11011,6 +11685,12 @@ "node": ">= 0.4" } }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/sharp": { "version": "0.35.4", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", @@ -11078,7 +11758,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "devOptional": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -11091,7 +11770,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -11101,7 +11779,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -11121,7 +11798,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -11138,7 +11814,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -11157,7 +11832,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -11251,6 +11925,15 @@ "dev": true, "license": "MIT" }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", @@ -11607,6 +12290,15 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tough-cookie": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", @@ -11738,6 +12430,37 @@ "node": ">= 0.8.0" } }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -11889,6 +12612,15 @@ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "license": "MIT" }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/unrs-resolver": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", @@ -12026,6 +12758,15 @@ } } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", @@ -12581,7 +13322,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "devOptional": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -12709,6 +13449,12 @@ "node": ">=0.10.0" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", @@ -12775,6 +13521,15 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, "node_modules/zod-validation-error": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", diff --git a/package.json b/package.json index 605e9cb..913e0af 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@date-fns/tz": "1.5.0", "@dnd-kit/core": "6.3.1", "@dnd-kit/utilities": "3.2.2", + "@modelcontextprotocol/sdk": "^1.30.0", "@prisma/adapter-pg": "7.10.0", "@prisma/client": "7.10.0", "@radix-ui/react-avatar": "1.2.6", diff --git a/src/server/aktionen/zeitbloecke.ts b/src/server/aktionen/zeitbloecke.ts index 19f68cc..a56001a 100644 --- a/src/server/aktionen/zeitbloecke.ts +++ b/src/server/aktionen/zeitbloecke.ts @@ -2,12 +2,7 @@ import { revalidatePath } from 'next/cache' import { z } from 'zod' -import { - zeitblockSchema, - zeitraumHash, - LAGE_FEHLER_TEXT, - lagePruefen, -} from '@/lib/zeitblock' +import { zeitraumHash, LAGE_FEHLER_TEXT, lagePruefen } from '@/lib/zeitblock' import { auditSchreiben } from '@/server/audit' import { NichtErlaubt, pruefeRolle } from '@/server/berechtigung' import { db } from '@/server/db' @@ -18,6 +13,7 @@ import { terminAnlegen, terminEntfernen, } from '@/server/kalender/schreiben' +import { betreffFuer, zeitblockAnlegenFuer } from '@/server/zeitblock/dienst' import { jetzt } from '@/server/zeitgeber' export type Rueckmeldung = @@ -56,11 +52,6 @@ async function eigenerZeitblock(id: string, benutzerId: string) { }) } -/** Was im Kalender als Betreff steht. */ -function betreffFuer(titel: string): string { - return titel -} - // --- Anlegen --------------------------------------------------------------- /** @@ -76,99 +67,28 @@ export async function zeitblockAnlegen(daten: FormData): Promise { if ('antwort' in geprueft) return geprueft.antwort const { akteur } = geprueft - const gelesen = zeitblockSchema.safeParse({ - aufgabeId: daten.get('aufgabeId'), - kalenderkontoId: daten.get('kalenderkontoId'), - beginn: daten.get('beginn'), - ende: daten.get('ende'), - }) - if (!gelesen.success) { - const lage = gelesen.error.issues.find((i) => i.path.includes('ende')) - return { ok: false, fehler: lage?.message ?? 'Die Angaben stimmen nicht.' } - } - - const [aufgabe, konto] = await Promise.all([ - db.aufgabe.findUnique({ - where: { id: gelesen.data.aufgabeId }, - select: { id: true, titel: true, notiz: true, status: true }, - }), - db.kalenderkonto.findFirst({ - where: { id: gelesen.data.kalenderkontoId, benutzerId: akteur.id, aktiv: true }, - select: { id: true, anbieter: true, kalenderId: true, anzeigename: true }, - }), - ]) - - if (!aufgabe) return { ok: false, fehler: 'Diese Aufgabe gibt es nicht.' } - if (!konto) return { ok: false, fehler: 'Dieses Kalenderkonto gibt es nicht.' } - - const zeitblock = await db.zeitblock.create({ - data: { - aufgabeId: aufgabe.id, - kalenderkontoId: konto.id, - beginn: gelesen.data.beginn, - ende: gelesen.data.ende, + const ergebnis = await zeitblockAnlegenFuer( + { id: akteur.id, email: akteur.email }, + { + aufgabeId: String(daten.get('aufgabeId') ?? ''), + kalenderkontoId: String(daten.get('kalenderkontoId') ?? ''), + beginn: new Date(String(daten.get('beginn') ?? '')), + ende: new Date(String(daten.get('ende') ?? '')), }, - select: { id: true }, - }) + ) - // Eine eingeplante Aufgabe ist geplant, nicht mehr bloß offen. - if (aufgabe.status === 'OFFEN') { - await db.aufgabe.update({ where: { id: aufgabe.id }, data: { status: 'GEPLANT' } }) - } - - let schreibfehler: string | null = null - try { - const token = await gueltigesTokenFuer(konto.id, jetzt()) - const ergebnis = await terminAnlegen({ - anbieter: konto.anbieter, - kalenderId: konto.kalenderId, - token, - betreff: betreffFuer(aufgabe.titel), - beschreibung: aufgabe.notiz, - beginn: gelesen.data.beginn, - ende: gelesen.data.ende, - }) - - await db.zeitblock.update({ - where: { id: zeitblock.id }, - data: { - extId: ergebnis.extId, - etag: ergebnis.etag, - hash: zeitraumHash(gelesen.data.beginn, gelesen.data.ende), - letzterFehler: null, - }, - }) - } catch (fehler) { - schreibfehler = - fehler instanceof Error ? fehler.message : 'Der Termin ließ sich nicht anlegen.' - await db.zeitblock.update({ - where: { id: zeitblock.id }, - data: { letzterFehler: schreibfehler }, - }) - } - - await auditSchreiben({ - akteur: { id: akteur.id, email: akteur.email }, - aktion: 'zeitblock.angelegt', - objektTyp: 'Zeitblock', - objektId: zeitblock.id, - details: { - aufgabe: aufgabe.titel, - kalender: konto.anzeigename, - geschrieben: schreibfehler === null, - }, - }) + if (!ergebnis.ok) return ergebnis revalidatePath('/woche') revalidatePath('/aufgaben') revalidatePath('/') - return schreibfehler - ? { + return ergebnis.geschrieben + ? { ok: true, id: ergebnis.id, meldung: `„${ergebnis.titel}" eingeplant.` } + : { ok: false, - fehler: `Eingeplant, aber nicht in den Kalender geschrieben: ${schreibfehler}`, + fehler: `Eingeplant, aber nicht in den Kalender geschrieben: ${ergebnis.fehler ?? 'unbekannter Fehler'}`, } - : { ok: true, id: zeitblock.id, meldung: `„${aufgabe.titel}" eingeplant.` } } // --- Verschieben ----------------------------------------------------------- diff --git a/src/server/zeitblock/dienst.ts b/src/server/zeitblock/dienst.ts new file mode 100644 index 0000000..889ead8 --- /dev/null +++ b/src/server/zeitblock/dienst.ts @@ -0,0 +1,132 @@ +import 'server-only' +import { zeitblockSchema, zeitraumHash } from '@/lib/zeitblock' +import { auditSchreiben } from '@/server/audit' +import { db } from '@/server/db' +import { gueltigesTokenFuer } from '@/server/kalender/abgleich' +import { terminAnlegen } from '@/server/kalender/schreiben' +import { jetzt } from '@/server/zeitgeber' + +/** + * Das Einplanen selbst — ohne Sitzung, ohne Formular. + * + * Die Serveraktion aus der Oberfläche und der MCP-Server benutzen dieselbe + * Stelle. Zwei Umsetzungen desselben Vorgangs wären zwei Gelegenheiten, die + * Regeln unterschiedlich hinzuschreiben — und der Kalenderschreibweg samt + * Schleifenschutz ist nichts, was man zweimal richtig hinbekommt. + * + * Bewusst **nicht** in einer Datei mit `'use server'`: Dort wird jede + * exportierte Funktion zu einem Endpunkt, den der Browser aufrufen kann. Eine + * Funktion, die den Akteur als Parameter nimmt, wäre dann ein offenes Tor. + */ + +export type Akteur = { id: string; email: string } + +export type Einplanung = { + aufgabeId: string + kalenderkontoId: string + beginn: Date + ende: Date +} + +export type Einplanungsergebnis = + | { ok: true; id: string; titel: string; geschrieben: boolean; fehler: string | null } + | { ok: false; fehler: string } + +/** Legt einen Zeitblock an und schreibt ihn in den Kalender. */ +export async function zeitblockAnlegenFuer( + akteur: Akteur, + eingabe: Einplanung, +): Promise { + const gelesen = zeitblockSchema.safeParse(eingabe) + if (!gelesen.success) { + const lage = gelesen.error.issues.find((i) => i.path.includes('ende')) + return { ok: false, fehler: lage?.message ?? 'Die Angaben stimmen nicht.' } + } + + const [aufgabe, konto] = await Promise.all([ + db.aufgabe.findUnique({ + where: { id: gelesen.data.aufgabeId }, + select: { id: true, titel: true, notiz: true, status: true }, + }), + db.kalenderkonto.findFirst({ + where: { id: gelesen.data.kalenderkontoId, benutzerId: akteur.id, aktiv: true }, + select: { id: true, anbieter: true, kalenderId: true, anzeigename: true }, + }), + ]) + + if (!aufgabe) return { ok: false, fehler: 'Diese Aufgabe gibt es nicht.' } + if (!konto) return { ok: false, fehler: 'Dieses Kalenderkonto gibt es nicht.' } + + const zeitblock = await db.zeitblock.create({ + data: { + aufgabeId: aufgabe.id, + kalenderkontoId: konto.id, + beginn: gelesen.data.beginn, + ende: gelesen.data.ende, + }, + select: { id: true }, + }) + + // Eine eingeplante Aufgabe ist geplant, nicht mehr bloß offen. + if (aufgabe.status === 'OFFEN') { + await db.aufgabe.update({ where: { id: aufgabe.id }, data: { status: 'GEPLANT' } }) + } + + let schreibfehler: string | null = null + try { + const token = await gueltigesTokenFuer(konto.id, jetzt()) + const ergebnis = await terminAnlegen({ + anbieter: konto.anbieter, + kalenderId: konto.kalenderId, + token, + betreff: betreffFuer(aufgabe.titel), + beschreibung: aufgabe.notiz, + beginn: gelesen.data.beginn, + ende: gelesen.data.ende, + }) + + await db.zeitblock.update({ + where: { id: zeitblock.id }, + data: { + extId: ergebnis.extId, + etag: ergebnis.etag, + hash: zeitraumHash(gelesen.data.beginn, gelesen.data.ende), + letzterFehler: null, + }, + }) + } catch (fehler) { + // Die Planung überlebt einen Netzhänger; der Zeitblock bleibt mit + // vermerktem Fehler stehen. + schreibfehler = + fehler instanceof Error ? fehler.message : 'Der Termin ließ sich nicht anlegen.' + await db.zeitblock.update({ + where: { id: zeitblock.id }, + data: { letzterFehler: schreibfehler }, + }) + } + + await auditSchreiben({ + akteur, + aktion: 'zeitblock.angelegt', + objektTyp: 'Zeitblock', + objektId: zeitblock.id, + details: { + aufgabe: aufgabe.titel, + kalender: konto.anzeigename, + geschrieben: schreibfehler === null, + }, + }) + + return { + ok: true, + id: zeitblock.id, + titel: aufgabe.titel, + geschrieben: schreibfehler === null, + fehler: schreibfehler, + } +} + +/** Der Betreff, unter dem ein Zeitblock im Kalender steht. */ +export function betreffFuer(titel: string): string { + return titel +} From bdc4260d59023a89448eb39547a67ea48642dc94 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sun, 13 Sep 2026 14:31:19 +0200 Subject: [PATCH 2/5] feat: MCP-Server ueber denselben Token wie die API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- e2e/mcp.spec.ts | 173 ++++++++++++++++++++ src/app/api/mcp/route.ts | 66 ++++++++ src/server/mcp/werkzeuge.ts | 306 ++++++++++++++++++++++++++++++++++++ 3 files changed, 545 insertions(+) create mode 100644 e2e/mcp.spec.ts create mode 100644 src/app/api/mcp/route.ts create mode 100644 src/server/mcp/werkzeuge.ts diff --git a/e2e/mcp.spec.ts b/e2e/mcp.spec.ts new file mode 100644 index 0000000..d62a9fd --- /dev/null +++ b/e2e/mcp.spec.ts @@ -0,0 +1,173 @@ +import { createHash, randomBytes } from 'node:crypto' +import { PrismaPg } from '@prisma/adapter-pg' +import { expect, test, type APIRequestContext } from '@playwright/test' +import { PrismaClient } from '../src/generated/prisma/client.js' +import { EMAIL, VERBINDUNG } from './hilfe' + +/** + * Der MCP-Endpunkt. + * + * Er spricht JSON-RPC, also wird hier auch JSON-RPC gesprochen — kein + * Nachbau der Bibliothek, sondern echte Aufrufe gegen den laufenden Server. + * + * Der wichtigste Punkt: Die Werkzeugliste hängt an den Rechten des Tokens. + * Was ein Token nicht darf, soll ein Modell gar nicht erst sehen. + */ + +const db = new PrismaClient({ adapter: new PrismaPg({ connectionString: VERBINDUNG }) }) +const PRAEFIX = `E2E-MCP-${Date.now()}` + +async function tokenBauen(benutzerId: string, rechte: string): Promise { + const praefix = randomBytes(4).toString('hex') + const klartext = `planer_${praefix}_${randomBytes(32).toString('base64url')}` + await db.apiToken.create({ + data: { + benutzerId, + name: `${PRAEFIX} ${rechte}`, + praefix, + hash: createHash('sha256').update(klartext, 'utf8').digest('hex'), + rechte, + }, + }) + return klartext +} + +type Antwort = { + result?: { tools?: { name: string }[]; content?: { text: string }[]; isError?: boolean } + error?: { message: string } +} + +/** Ein JSON-RPC-Aufruf gegen den Endpunkt. */ +async function rufen( + request: APIRequestContext, + token: string | null, + methode: string, + parameter: Record = {}, +) { + const antwort = await request.post('/api/mcp', { + headers: { + 'Content-Type': 'application/json', + // Der Streamable-HTTP-Transport besteht auf beidem. + Accept: 'application/json, text/event-stream', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + data: { jsonrpc: '2.0', id: 1, method: methode, params: parameter }, + }) + return antwort +} + +/** Der Handschlag, den jede MCP-Verbindung zuerst führt. */ +const HANDSCHLAG = { + protocolVersion: '2025-06-18', + capabilities: {}, + clientInfo: { name: 'e2e', version: '1.0.0' }, +} + +test.describe('MCP', () => { + test.skip(!EMAIL || !VERBINDUNG, 'Ohne ADMIN_EMAIL und DATABASE_URL kein Testkonto.') + + let benutzerId = '' + + test.beforeAll(async () => { + const benutzer = await db.benutzer.findUniqueOrThrow({ where: { email: EMAIL } }) + benutzerId = benutzer.id + }) + + test.afterAll(async () => { + await db.aufgabe.deleteMany({ where: { titel: { startsWith: PRAEFIX } } }) + await db.apiToken.deleteMany({ where: { name: { startsWith: PRAEFIX } } }) + await db.auditEintrag.deleteMany({ where: { aktion: { startsWith: 'mcp.' } } }) + await db.$disconnect() + }) + + test('weist eine Anfrage ohne Token ab', async ({ request }) => { + const antwort = await rufen(request, null, 'initialize', HANDSCHLAG) + expect(antwort.status()).toBe(401) + }) + + test('führt den Handschlag mit gültigem Token', async ({ request }) => { + const token = await tokenBauen(benutzerId, 'lesen') + const antwort = await rufen(request, token, 'initialize', HANDSCHLAG) + + expect(antwort.ok()).toBe(true) + const inhalt = (await antwort.json()) as { + result?: { serverInfo?: { name: string } } + } + expect(inhalt.result?.serverInfo?.name).toBe('planer') + }) + + test('zeigt nur die Werkzeuge, die der Token erlaubt', async ({ request }) => { + const nurLesen = await tokenBauen(benutzerId, 'lesen') + await rufen(request, nurLesen, 'initialize', HANDSCHLAG) + + const antwort = await rufen(request, nurLesen, 'tools/list') + const namen = (((await antwort.json()) as Antwort).result?.tools ?? []).map( + (w) => w.name, + ) + + expect(namen).toContain('aufgaben_suchen') + expect(namen).toContain('woche_ansehen') + // Erfassen und Schreiben fehlen dem Token – und damit der Liste. + expect(namen).not.toContain('aufgabe_anlegen') + expect(namen).not.toContain('aufgabe_abhaken') + expect(namen).not.toContain('zeitblock_anlegen') + }) + + test('zeigt einem Erfassen-Token nur das Anlegen', async ({ request }) => { + const token = await tokenBauen(benutzerId, 'erfassen') + await rufen(request, token, 'initialize', HANDSCHLAG) + + const antwort = await rufen(request, token, 'tools/list') + const namen = (((await antwort.json()) as Antwort).result?.tools ?? []).map( + (w) => w.name, + ) + + expect(namen).toEqual(['aufgabe_anlegen']) + }) + + test('legt eine Aufgabe an und protokolliert das', async ({ request }) => { + const token = await tokenBauen(benutzerId, 'erfassen') + await rufen(request, token, 'initialize', HANDSCHLAG) + + const titel = `${PRAEFIX} Angebot schreiben` + const antwort = await rufen(request, token, 'tools/call', { + name: 'aufgabe_anlegen', + arguments: { titel, aufwandMinuten: 45 }, + }) + + expect(antwort.ok()).toBe(true) + const inhalt = (await antwort.json()) as Antwort + expect(inhalt.result?.content?.[0]?.text).toContain('Angelegt') + + const aufgabe = await db.aufgabe.findFirstOrThrow({ + where: { titel }, + select: { quelle: true, bereichId: true, status: true, aufwandMinuten: true }, + }) + expect(aufgabe.quelle).toBe('MCP') + expect(aufgabe.aufwandMinuten).toBe(45) + // Wie über die Schnittstelle: offen und in der Inbox. + expect(aufgabe.status).toBe('OFFEN') + expect(aufgabe.bereichId).toBeNull() + + const eintrag = await db.auditEintrag.findFirst({ + where: { aktion: 'mcp.erfasst' }, + select: { details: true }, + }) + expect(JSON.stringify(eintrag?.details)).toContain('mcp') + }) + + test('lässt ein Werkzeug ohne Recht gar nicht erst aufrufen', async ({ request }) => { + const token = await tokenBauen(benutzerId, 'lesen') + await rufen(request, token, 'initialize', HANDSCHLAG) + + const antwort = await rufen(request, token, 'tools/call', { + name: 'aufgabe_anlegen', + arguments: { titel: `${PRAEFIX} darf nicht` }, + }) + + const inhalt = (await antwort.json()) as Antwort + // Unbekanntes Werkzeug: Es ist für diesen Token nicht registriert. + expect(inhalt.error ?? inhalt.result?.isError).toBeTruthy() + expect(await db.aufgabe.count({ where: { titel: `${PRAEFIX} darf nicht` } })).toBe(0) + }) +}) diff --git a/src/app/api/mcp/route.ts b/src/app/api/mcp/route.ts new file mode 100644 index 0000000..9b38df6 --- /dev/null +++ b/src/app/api/mcp/route.ts @@ -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 { + 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) +} diff --git a/src/server/mcp/werkzeuge.ts b/src/server/mcp/werkzeuge.ts new file mode 100644 index 0000000..95b474b --- /dev/null +++ b/src/server/mcp/werkzeuge.ts @@ -0,0 +1,306 @@ +import 'server-only' +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { z } from 'zod' +import { darf } from '@/lib/api' +import { alsDatum, alsDauer, alsUhrzeitAnzeige, wochenBeginn } from '@/lib/zeit' +import { auditSchreiben } from '@/server/audit' +import type { Aufrufer } from '@/server/api/token' +import { db } from '@/server/db' +import { zeitblockAnlegenFuer } from '@/server/zeitblock/dienst' +import { jetzt } from '@/server/zeitgeber' + +/** + * Der MCP-Server des Planers. + * + * Er hängt an denselben Tokens und denselben Rechten wie die Schnittstelle + * aus M7 — ein Sprachmodell bekommt keinen eigenen, weicheren Zugang. Was + * ein Token nicht darf, kann auch ein Werkzeug nicht. + * + * Die Werkzeuge sind bewusst wenige und grob geschnitten. Ein Modell, das + * zwischen zwanzig fast gleichen Werkzeugen wählen muss, wählt falsch; und + * jedes zusätzliche Werkzeug ist eine weitere Möglichkeit, versehentlich + * etwas zu ändern. + */ + +/** Antwort in der Form, die MCP erwartet. */ +function text(inhalt: string) { + return { content: [{ type: 'text' as const, text: inhalt }] } +} + +function fehler(inhalt: string) { + return { content: [{ type: 'text' as const, text: inhalt }], isError: true } +} + +const STATUS_OFFEN = ['OFFEN', 'GEPLANT', 'WARTET'] as const + +/** Wie viele Datensätze ein Werkzeug höchstens zurückgibt. */ +const GRENZE = 50 + +export function serverBauen(aufrufer: Aufrufer): McpServer { + const server = new McpServer( + { name: 'planer', version: '1.0.0' }, + { + instructions: [ + 'Der Planer verwaltet Aufgaben, Ideen, Projekte und Zeitblöcke einer', + 'einzelnen Person. Aufgaben, die über diese Werkzeuge entstehen,', + 'landen in der Inbox und werden von Hand einsortiert — setze also', + 'keinen Bereich und kein Projekt, das geht hier ohnehin nicht.', + 'Zeiten sind Europe/Berlin.', + ].join(' '), + }, + ) + + // --- Lesen --------------------------------------------------------------- + + if (darf(aufrufer.rechte, 'lesen')) { + server.registerTool( + 'aufgaben_suchen', + { + title: 'Aufgaben suchen', + description: + 'Listet Aufgaben. Ohne Angaben die offenen, nach Fälligkeit sortiert.', + inputSchema: { + suche: z.string().max(100).optional().describe('Text im Titel'), + nurOffene: z.boolean().default(true), + nurUeberfaellige: z.boolean().default(false), + }, + }, + async ({ suche, nurOffene, nurUeberfaellige }) => { + const zeitpunkt = jetzt() + const aufgaben = await db.aufgabe.findMany({ + where: { + typ: { not: 'IDEE' }, + ...(nurOffene ? { status: { in: [...STATUS_OFFEN] } } : {}), + ...(nurUeberfaellige ? { faelligAm: { lt: zeitpunkt } } : {}), + ...(suche ? { titel: { contains: suche, mode: 'insensitive' } } : {}), + }, + orderBy: [{ faelligAm: { sort: 'asc', nulls: 'last' } }, { prioritaet: 'asc' }], + take: GRENZE, + select: { + id: true, + titel: true, + status: true, + prioritaet: true, + aufwandMinuten: true, + faelligAm: true, + bereich: { select: { name: true } }, + }, + }) + + if (aufgaben.length === 0) return text('Keine Aufgabe passt dazu.') + + const zeilen = aufgaben.map((a) => { + const teile = [ + a.titel, + `(${a.id})`, + a.status, + `Prio ${a.prioritaet}`, + a.aufwandMinuten ? alsDauer(a.aufwandMinuten) : 'ohne Schätzung', + a.faelligAm ? `fällig ${alsDatum(a.faelligAm)}` : 'ohne Termin', + a.bereich?.name ?? 'Inbox', + ] + return `- ${teile.join(' · ')}` + }) + return text(zeilen.join('\n')) + }, + ) + + server.registerTool( + 'woche_ansehen', + { + title: 'Woche ansehen', + description: + 'Zeigt die eingeplanten Zeitblöcke einer Woche samt freier Kapazität.', + inputSchema: { + versatz: z + .number() + .int() + .min(-52) + .max(52) + .default(0) + .describe('0 = diese Woche, 1 = nächste'), + }, + }, + async ({ versatz }) => { + const zeitpunkt = new Date(jetzt().getTime() + versatz * 7 * 86_400_000) + const von = wochenBeginn(zeitpunkt) + const bis = new Date(von.getTime() + 7 * 86_400_000) + + const bloecke = await db.zeitblock.findMany({ + where: { + kalenderkonto: { benutzerId: aufrufer.benutzerId }, + beginn: { gte: von, lt: bis }, + }, + orderBy: { beginn: 'asc' }, + select: { + beginn: true, + ende: true, + aufgabe: { select: { titel: true } }, + }, + }) + + if (bloecke.length === 0) { + return text(`Ab ${alsDatum(von)} ist noch nichts eingeplant.`) + } + + const zeilen = bloecke.map( + (b) => + `- ${alsDatum(b.beginn)} ${alsUhrzeitAnzeige(b.beginn)}–${alsUhrzeitAnzeige(b.ende)} ${b.aufgabe.titel}`, + ) + const summe = bloecke.reduce( + (s, b) => s + (b.ende.getTime() - b.beginn.getTime()) / 60_000, + 0, + ) + return text(`${zeilen.join('\n')}\n\nZusammen ${alsDauer(summe)}.`) + }, + ) + + server.registerTool( + 'kalender_auflisten', + { + title: 'Kalender auflisten', + description: + 'Die verbundenen Kalenderkonten. Ihre Kennung braucht das Einplanen.', + inputSchema: {}, + }, + async () => { + const konten = await db.kalenderkonto.findMany({ + where: { benutzerId: aufrufer.benutzerId, aktiv: true }, + orderBy: { erstelltAm: 'asc' }, + select: { id: true, anzeigename: true, anbieter: true }, + }) + if (konten.length === 0) return text('Kein Kalender verbunden.') + return text(konten.map((k) => `- ${k.anzeigename} (${k.id})`).join('\n')) + }, + ) + } + + // --- Erfassen ------------------------------------------------------------ + + if (darf(aufrufer.rechte, 'erfassen')) { + server.registerTool( + 'aufgabe_anlegen', + { + title: 'Aufgabe anlegen', + description: + 'Legt eine Aufgabe in der Inbox an. Bereich und Projekt lassen sich nicht setzen.', + inputSchema: { + titel: z.string().trim().min(1).max(200), + notiz: z.string().trim().max(4000).optional(), + faelligAm: z.iso.date().optional().describe('JJJJ-MM-TT'), + aufwandMinuten: z.number().int().min(1).max(1440).optional(), + prioritaet: z + .number() + .int() + .min(1) + .max(5) + .optional() + .describe('1 dringend … 5 irgendwann'), + }, + }, + async ({ titel, notiz, faelligAm, aufwandMinuten, prioritaet }) => { + const aufgabe = await db.aufgabe.create({ + data: { + titel, + notiz: notiz ?? null, + quelle: 'MCP', + faelligAm: faelligAm ? new Date(faelligAm) : null, + ...(aufwandMinuten ? { aufwandMinuten } : {}), + ...(prioritaet ? { prioritaet } : {}), + }, + select: { id: true, titel: true }, + }) + + await protokoll(aufrufer, 'mcp.erfasst', aufgabe.id, { titel: aufgabe.titel }) + return text(`Angelegt: „${aufgabe.titel}" (${aufgabe.id}). Liegt in der Inbox.`) + }, + ) + } + + // --- Schreiben ----------------------------------------------------------- + + if (darf(aufrufer.rechte, 'schreiben')) { + server.registerTool( + 'aufgabe_abhaken', + { + title: 'Aufgabe abhaken', + description: 'Setzt eine Aufgabe auf erledigt.', + inputSchema: { id: z.string().min(1) }, + }, + async ({ id }) => { + const aufgabe = await db.aufgabe.findUnique({ + where: { id }, + select: { id: true, titel: true, status: true }, + }) + if (!aufgabe) return fehler('Diese Aufgabe gibt es nicht.') + if (aufgabe.status === 'ERLEDIGT') return text('War schon erledigt.') + + await db.aufgabe.update({ + where: { id: aufgabe.id }, + data: { status: 'ERLEDIGT', erledigtAm: jetzt() }, + }) + await protokoll(aufrufer, 'mcp.abgehakt', aufgabe.id, { titel: aufgabe.titel }) + return text(`Abgehakt: „${aufgabe.titel}".`) + }, + ) + + server.registerTool( + 'zeitblock_anlegen', + { + title: 'Aufgabe einplanen', + description: + 'Legt einen Zeitblock für eine Aufgabe an und schreibt ihn in den Kalender.', + inputSchema: { + aufgabeId: z.string().min(1), + kalenderkontoId: z.string().min(1).describe('Kennung aus kalender_auflisten'), + beginn: z.iso.datetime().describe('ISO 8601 mit Zeitzone'), + ende: z.iso.datetime(), + }, + }, + async ({ aufgabeId, kalenderkontoId, beginn, ende }) => { + // Derselbe Weg wie beim Ziehen in der Wochenansicht: Rasterprüfung, + // Kalenderschreibweg, Schleifenschutz, Protokoll. + const ergebnis = await zeitblockAnlegenFuer( + { id: aufrufer.benutzerId, email: aufrufer.email }, + { + aufgabeId, + kalenderkontoId, + beginn: new Date(beginn), + ende: new Date(ende), + }, + ) + + if (!ergebnis.ok) return fehler(ergebnis.fehler) + + return text( + ergebnis.geschrieben + ? `Eingeplant: „${ergebnis.titel}".` + : `Eingeplant, aber nicht in den Kalender geschrieben: ${ergebnis.fehler ?? 'unbekannter Fehler'}`, + ) + }, + ) + } + + return server +} + +/** + * Hält fest, was über MCP passiert ist. + * + * Ein Sprachmodell, das Aufgaben anlegt, muss genauso nachvollziehbar sein + * wie ein Mensch — eher noch mehr. + */ +async function protokoll( + aufrufer: Aufrufer, + aktion: string, + objektId: string, + details: Record, +): Promise { + await auditSchreiben({ + akteur: { id: aufrufer.benutzerId, email: aufrufer.email }, + aktion, + objektTyp: 'Aufgabe', + objektId, + details: { ueber: 'mcp', token: aufrufer.tokenId, ...details }, + }) +} From 0ccb21303d19d6685721d79d585d93c89bccc1a4 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sun, 13 Sep 2026 15:19:30 +0200 Subject: [PATCH 3/5] docs: MCP beschreiben, Fahrplan abschliessen Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 23 +++++++++ README.md | 7 ++- ROADMAP.md | 18 +++++-- docs/MCP.md | 83 ++++++++++++++++++++++++++++++ docs/handbuch/README.md | 11 +--- src/server/aktionen/zeitbloecke.ts | 19 +++++-- 6 files changed, 141 insertions(+), 20 deletions(-) create mode 100644 docs/MCP.md diff --git a/CHANGELOG.md b/CHANGELOG.md index cff08f7..7205030 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,29 @@ die Versionierung [Semantic Versioning](https://semver.org/lang/de/). ## [Unveröffentlicht] +### Hinzugefügt + +- **MCP-Server** unter `/api/mcp` mit sechs Werkzeugen: Aufgaben suchen, + Woche ansehen, Kalender auflisten, Aufgabe anlegen, abhaken und einplanen. + Beschreibung in [docs/MCP.md](docs/MCP.md). +- Die Werkzeugliste richtet sich nach den Rechten des Tokens — was ein Token + nicht darf, sieht das Modell gar nicht erst. + +### Geändert + +- Das Einplanen liegt jetzt an einer Stelle, die sowohl die Oberfläche als + auch der MCP-Server benutzen. Ein über MCP angelegter Zeitblock verhält + sich damit genau wie ein von Hand gezogener. + +### Sicherheit + +- MCP benutzt dieselben Tokens und dieselben Rechte wie die Schnittstelle. + Ein Sprachmodell bekommt keinen eigenen Zugang. +- Jede schreibende Nutzung steht im Protokoll, mit dem Vermerk, dass sie + über MCP kam. +- Der Server kann weder löschen noch Bereiche, Projekte, Kalenderkonten, + Einstellungen oder Tokens anfassen. + ## [0.7.0] – 2026-09-13 ### Hinzugefügt diff --git a/README.md b/README.md index d9d5393..cce4eac 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ wird. Planer lässt sich als App auf dem Startbildschirm ablegen - **Öffentliche Schnittstelle** mit Tokens je Anwendung, Rechten je Token und jeder schreibenden Nutzung im Protokoll ([Beschreibung](docs/API.md)) +- **MCP-Server** für Sprachmodelle — dieselben Tokens, dieselben Rechte + ([Beschreibung](docs/MCP.md)) - Anmeldung über einen OIDC-Anbieter, über Passkeys oder mit E-Mail und Passwort — je Instanz einzeln zuschaltbar - Rollen `Benutzer` und `Verwaltung`; jede schreibende Aktion wird @@ -51,9 +53,10 @@ wird. - Hell und Dunkel, Oberfläche durchgehend auf Deutsch - Betrieb über Docker Compose, mit Sicherung, Aktualisierung und Healthcheck -### Geplant +### Was noch kommen könnte -Ein MCP-Server. Siehe [ROADMAP.md](ROADMAP.md). +Der [Fahrplan](ROADMAP.md) ist abgearbeitet. Was danach denkbar wäre, steht +dort unter „Ideen für später" — und was bewusst nicht kommt, gleich darunter. ## Bildergalerie diff --git a/ROADMAP.md b/ROADMAP.md index 026814a..35c1446 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -12,7 +12,7 @@ grünen Prüfungen, aktueller Dokumentation und einem Eintrag im DEVLOG. | [M5](#m5--benachrichtigungen) | Benachrichtigungen | erledigt | | [M6](#m6--integrationen) | Integrationen | erledigt | | [M7](#m7--schnellerfassung-und-api) | Schnellerfassung und API | erledigt | -| [M8](#m8--mcp) | MCP | geplant | +| [M8](#m8--mcp) | MCP | erledigt | --- @@ -156,9 +156,19 @@ Zeremonie als Schutz. ## M8 – MCP -- [ ] MCP-Server über `@modelcontextprotocol/sdk` -- [ ] Lesen und Anlegen von Aufgaben und Zeitblöcken -- [ ] Rechte wie bei der API, jede Nutzung im Protokoll +**Erledigt am 13.09.2026 mit Version 0.8.0.** + +**Ziel:** Ein Sprachmodell soll den Alltag erleichtern können — ohne einen +eigenen, weicheren Zugang zu bekommen als jede andere fremde Anwendung. + +- [x] MCP-Server über `@modelcontextprotocol/sdk` +- [x] Lesen und Anlegen von Aufgaben und Zeitblöcken +- [x] Rechte wie bei der API, jede schreibende Nutzung im Protokoll +- [x] Werkzeugliste richtet sich nach den Rechten des Tokens + +**Bewusst offen gelassen:** Der Server kann nichts löschen, keine Bereiche +oder Projekte anlegen und keine Einstellungen anfassen. Ein Modell soll den +Alltag erleichtern, nicht die Ordnung umbauen. --- diff --git a/docs/MCP.md b/docs/MCP.md new file mode 100644 index 0000000..1ab1ac9 --- /dev/null +++ b/docs/MCP.md @@ -0,0 +1,83 @@ +# MCP-Server + +Der Planer spricht das Model Context Protocol. Damit kann ein Sprachmodell +Aufgaben suchen, anlegen, abhaken und einplanen — mit denselben Tokens und +denselben Rechten wie die [Schnittstelle](API.md). + +Adresse: `/api/mcp` + +## Einrichten + +1. In der Anwendung unter **API** einen Token anlegen. Gib ihm nur, was das + Modell wirklich braucht. +2. Den Client auf die Adresse zeigen lassen, mit dem Token als Bearer. + +Für Claude Code: + +```bash +claude mcp add planer --transport http https://planer.example.de/api/mcp \ + --header "Authorization: Bearer planer_…" +``` + +Für Clients, die nur stdio können, dazwischen `mcp-remote` setzen: + +```json +{ + "mcpServers": { + "planer": { + "command": "npx", + "args": [ + "-y", + "mcp-remote", + "https://planer.example.de/api/mcp", + "--header", + "Authorization: Bearer planer_…" + ] + } + } +} +``` + +## Werkzeuge + +Welche ein Modell zu sehen bekommt, hängt am Token. Was nicht erlaubt ist, +taucht gar nicht erst in der Liste auf — ein Modell soll nicht gegen eine Tür +laufen, die für es verschlossen ist. + +| Werkzeug | Recht | Was es tut | +| -------------------- | ----------- | ----------------------------------------------- | +| `aufgaben_suchen` | `lesen` | Aufgaben auflisten, optional gefiltert | +| `woche_ansehen` | `lesen` | Zeitblöcke einer Woche mit Summe | +| `kalender_auflisten` | `lesen` | Verbundene Kalender samt Kennung | +| `aufgabe_anlegen` | `erfassen` | Neue Aufgabe in der Inbox | +| `aufgabe_abhaken` | `schreiben` | Aufgabe auf erledigt setzen | +| `zeitblock_anlegen` | `schreiben` | Aufgabe einplanen und in den Kalender schreiben | + +Zum Einplanen braucht es die Kennung eines Kalenderkontos — daher erst +`kalender_auflisten`, dann `zeitblock_anlegen`. + +`zeitblock_anlegen` geht durch dieselbe Stelle wie das Ziehen in der +Wochenansicht: Rasterprüfung, Kalenderschreibweg, Schleifenschutz, Protokoll. +Ein über MCP angelegter Zeitblock verhält sich also genau wie ein von Hand +gezogener. + +## Was der Server bewusst **nicht** kann + +- **Löschen.** Weder Aufgaben noch Zeitblöcke. Abhaken ja, wegräumen nein. +- **Bereich oder Projekt setzen.** Was hier entsteht, landet in der Inbox. +- **Bereiche, Projekte oder Kalenderkonten anlegen und ändern.** +- **Einstellungen oder Tokens anfassen.** + +Ein Modell soll den Alltag erleichtern, nicht die Ordnung umbauen. + +## Protokoll + +Jede schreibende Nutzung landet im Protokoll — mit dem Vermerk, dass sie über +MCP kam, und mit dem Token. Ein Modell, das Aufgaben anlegt, muss genauso +nachvollziehbar sein wie ein Mensch. Eher noch mehr. + +## Ohne Sitzungen + +Der Server ist zustandslos: 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. diff --git a/docs/handbuch/README.md b/docs/handbuch/README.md index efd439f..be1f0ea 100644 --- a/docs/handbuch/README.md +++ b/docs/handbuch/README.md @@ -31,18 +31,9 @@ Anleitung für den Alltag mit dem Planer. Eine Seite je Thema. 11. **[Protokoll](protokoll.md)** – nachsehen, wer wann was geändert hat -## Was noch kommt - -Diese Seiten entstehen mit den nächsten Meilensteinen: - -| Thema | Meilenstein | -| ----- | ----------- | -| MCP | M8 | - -Der [Fahrplan](../../ROADMAP.md) sagt, was in welchem Meilenstein steckt. - --- Für fremde Anwendungen und Skripte: [Schnittstelle](../API.md). +Für Sprachmodelle: [MCP-Server](../MCP.md). Zur Einrichtung einer neuen Instanz: [Schnellstart](../SCHNELLSTART.md). Für Betrieb, Sicherung und alle Umgebungsvariablen: [README](../../README.md). diff --git a/src/server/aktionen/zeitbloecke.ts b/src/server/aktionen/zeitbloecke.ts index a56001a..f98e020 100644 --- a/src/server/aktionen/zeitbloecke.ts +++ b/src/server/aktionen/zeitbloecke.ts @@ -70,10 +70,10 @@ export async function zeitblockAnlegen(daten: FormData): Promise { const ergebnis = await zeitblockAnlegenFuer( { id: akteur.id, email: akteur.email }, { - aufgabeId: String(daten.get('aufgabeId') ?? ''), - kalenderkontoId: String(daten.get('kalenderkontoId') ?? ''), - beginn: new Date(String(daten.get('beginn') ?? '')), - ende: new Date(String(daten.get('ende') ?? '')), + aufgabeId: feld(daten, 'aufgabeId'), + kalenderkontoId: feld(daten, 'kalenderkontoId'), + beginn: new Date(feld(daten, 'beginn')), + ende: new Date(feld(daten, 'ende')), }, ) @@ -91,6 +91,17 @@ export async function zeitblockAnlegen(daten: FormData): Promise { } } +/** + * Ein Textfeld aus dem Formular. + * + * FormData kann auch Dateien liefern; die haetten hier nichts zu suchen und + * wuerden beim Stringify zu "[object Object]". + */ +function feld(daten: FormData, name: string): string { + const wert = daten.get(name) + return typeof wert === 'string' ? wert : '' +} + // --- Verschieben ----------------------------------------------------------- const verschieben = z.object({ From 96e105fe923cd279d73febaa559c1107060fd392 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sun, 13 Sep 2026 15:21:00 +0200 Subject: [PATCH 4/5] docs: DEVLOG-Eintrag fuer M8 Co-Authored-By: Claude Opus 5 --- devlog/DEVLOG.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++- devlog/zeiten.csv | 1 + 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/devlog/DEVLOG.md b/devlog/DEVLOG.md index eab765b..bce32f9 100644 --- a/devlog/DEVLOG.md +++ b/devlog/DEVLOG.md @@ -14,7 +14,80 @@ die Übersicht aus `scripts/zeit.sh summe`. | M5 | claude | 420 | 7,00 | | M6 | claude | 733 | 12,22 | | M7 | claude | 886 | 14,77 | -| **Gesamt** | | **2452** | **40,87** | +| M8 | claude | 69 | 1,15 | +| **Gesamt** | | **2521** | **42,02** | + +--- + +## 2026-09-13 – M8: MCP + +**Dauer:** 69 Minuten (14:10–15:19), Art `claude`. + +> Der kürzeste Meilenstein — weil das Fundament schon stand. Tokens, Rechte +> und Protokoll kamen mit M7, der Kalenderschreibweg mit M4. Übrig blieb das +> Zusammenstecken. + +**Ziel der Sitzung:** Ein Sprachmodell soll den Alltag erleichtern können — +ohne einen eigenen, weicheren Zugang zu bekommen als jede andere fremde +Anwendung. + +### Erledigt + +- MCP-Server unter `/api/mcp` über `@modelcontextprotocol/sdk` +- Sechs Werkzeuge: suchen, Woche ansehen, Kalender auflisten, anlegen, + abhaken, einplanen +- Das Einplanen aus der Serveraktion herausgelöst, damit Oberfläche und MCP + dieselbe Stelle benutzen +- 6 neue E2E-Tests, Beschreibung in `docs/MCP.md` + +### Entscheidungen mit Begründung + +**Dieselben Tokens, dieselben Rechte wie die Schnittstelle.** Ein +Sprachmodell ist aus Sicht des Planers eine fremde Anwendung wie jede andere. +Ein eigener Zugangsweg wäre ein zweiter Satz Regeln — und der zweite Satz ist +immer der, den man vergisst zu pflegen. + +**Die Werkzeugliste richtet sich nach den Rechten.** Was ein Token nicht +darf, taucht gar nicht erst auf. Ein Modell soll nicht gegen eine Tür laufen, +die für es verschlossen ist; und eine Fehlermeldung als Antwort auf einen +Werkzeugaufruf ist verschwendeter Kontext. + +**Wenige, grob geschnittene Werkzeuge.** Ein Modell, das zwischen zwanzig +fast gleichen wählen muss, wählt falsch. Jedes weitere Werkzeug ist zudem +eine zusätzliche Möglichkeit, versehentlich etwas zu ändern. + +**Der Server kann nichts löschen.** Abhaken ja, wegräumen nein. Auch keine +Bereiche, Projekte oder Einstellungen anfassen. Ein Modell soll den Alltag +erleichtern, nicht die Ordnung umbauen. + +**Das Einplanen liegt jetzt an einer gemeinsamen Stelle** — und die liegt +bewusst **nicht** in einer Datei mit `'use server'`. Dort wird jede +exportierte Funktion zu einem Endpunkt, den der Browser aufrufen kann; eine +Funktion, die den Akteur als Parameter nimmt, wäre dann ein offenes Tor. + +**Zustandslos.** Der Planer hat keine langlaufenden Vorgänge, und eine +Sitzungsverwaltung wäre Zustand, den mehrere Container nicht teilen könnten. + +### Probleme und Lösungen + +| Problem | Lösung | +| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| Der SDK-Transport erwartet Node-Streams, Next liefert `Request` | Das SDK bringt seit 1.30 `WebStandardStreamableHTTPServerTransport` mit — `handleRequest(Request): Promise` passt direkt. | +| ESLint beanstandete das Auslesen der Formularfelder | `FormData.get` kann auch eine Datei liefern; die würde beim Stringify zu `[object Object]`. Ein Helfer nimmt jetzt nur Text. | + +### Offene Punkte + +- Der Server ist gegen die eigenen Tests geprüft, nicht gegen einen echten + Client. Ein Lauf mit Claude Code gegen die laufende Instanz wäre der + Beweis, dass Handschlag und Werkzeugbeschreibungen auch dort taugen. +- Die Handbuchbilder sind unverändert aus M7 — der MCP-Server hat keine + Oberfläche, es gibt nichts abzubilden. + +### Commits + +- 5f45dff refactor: Einplanen aus der Serveraktion herauslösen +- bdc4260 feat: MCP-Server über denselben Token wie die API +- (docs) MCP beschreiben, Fahrplan abschließen --- diff --git a/devlog/zeiten.csv b/devlog/zeiten.csv index 5db23a6..8006ba5 100644 --- a/devlog/zeiten.csv +++ b/devlog/zeiten.csv @@ -6,3 +6,4 @@ datum;start;ende;minuten;meilenstein;art;notiz 2026-09-11;06:35;13:35;420;M5;claude;M5 Benachrichtigungen fertig 2026-09-11;20:47;09:00;733;M6;claude;M6 Integrationen fertig 2026-09-12;10:15;01:01;886;M7;claude;M7 Schnellerfassung und API fertig +2026-09-13;14:10;15:19;69;M8;claude;M8 MCP fertig From 6d69eebd4d2219db0066192d9e36cc0fba2385ad Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sun, 13 Sep 2026 15:24:10 +0200 Subject: [PATCH 5/5] chore(release): 0.8.0 Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 2 ++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7205030..5b32c53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ die Versionierung [Semantic Versioning](https://semver.org/lang/de/). ## [Unveröffentlicht] +## [0.8.0] – 2026-09-13 + ### Hinzugefügt - **MCP-Server** unter `/api/mcp` mit sechs Werkzeugen: Aufgaben suchen, diff --git a/package-lock.json b/package-lock.json index c891339..7c66279 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "planer", - "version": "0.7.0", + "version": "0.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "planer", - "version": "0.7.0", + "version": "0.8.0", "dependencies": { "@auth/prisma-adapter": "2.11.3", "@date-fns/tz": "1.5.0", diff --git a/package.json b/package.json index 913e0af..3d92cad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "planer", - "version": "0.7.0", + "version": "0.8.0", "private": true, "description": "Aufgaben, Ideen, Projekte und Zeitblöcke mit Anbindung an Microsoft 365 und Google Kalender", "type": "module",