feat(mcp): Ressourcen und Prompts fuer Claude Desktop, zwei Befunde behoben
MCP-ADAPTER 1.1
Werkzeuge ruft ein Modell von sich aus auf - Ressourcen und Prompts waehlt ein
MENSCH. In Claude Desktop sind das Anhang-Menue und Schraegstrich-Befehle, und
genau die fehlten.
- Vier Ressourcen: dc://roadmap (fertiger Markdown-Block),
dc://naechste-schritte, dc://projekte, dc://befunde.
- Vier Prompts: arbeit-aufnehmen, befunde-sichten, fehler-melden,
roadmap-pflegen. Ihr Wert liegt nicht in der gesparten Tipparbeit, sondern
darin, dass die Hausregeln mitkommen - erst nachsehen wer dransitzt,
Fremdmeldung nur mit Begruendung, Verworfenes nicht ohne neues Argument
wiedervorschlagen. Bisher standen die nur in Werkzeugbeschreibungen, und
die liest ein Modell erst, wenn es das Werkzeug schon gewaehlt hat.
- DC_PROJECT: worauf die Ressourcen zeigen. Werkzeuge nehmen project_slug je
Aufruf, Ressourcen haben keine Argumente.
- Ressourcen laufen durch dieselbe Fremddaten-Abgrenzung wie
Werkzeugergebnisse - sie sind eher ein Einfallstor, weil sie ungefragt im
Kontext landen.
- Einrichtung fuer claude_desktop_config.json dokumentiert.
Behoben: "initialize" spiegelte die verlangte Protokollfassung zurueck. Ein
Client, der 2025-06-18 anfragte, bekam sie bestaetigt, obwohl der Adapter
fuer 2024-11-05 geschrieben war. Jetzt wird gegen eine Liste tatsaechlich
bedienter Fassungen ausgehandelt.
Behoben: process.exit() mitten in schliessenden libuv-Handles. Unter Windows
eine Assertion und Rueckgabewert != 0 - ein sauber beendeter Server sah aus
wie ein abgestuerzter.
ZWEI BEFUNDE, DIE ERST DIE RESSOURCEN ZUTAGE FOERDERTEN
- roadmap.php?action=render lehnte fuer ein Werkbank-Token mit
"missing_project" ab. projectFilter() laesst es beim Lesen bewusst durch
(es soll die Landschaft sehen) und liefert null, also wurde der
Projektbezug "all". Betroffen war ausgerechnet der Tokentyp, den der
Adapter verlangt. Rueckfall ist jetzt das Heimatprojekt des Tokens.
- ProjectLinks::between() benutzte :a dreimal, forProject() :slug zweimal.
Bei ATTR_EMULATE_PREPARES = false unzulaessig -> HY093 und ein 500 auf
/api/bugtracker/v1/projects. Derselbe Fehler wie einst in der
Volltextsuche des Bugtrackers; ein Suchlauf ueber alle prepare()-Aufrufe
der Codebasis fand keine weitere Stelle.
WARUM BEIDE UNBEMERKT BLIEBEN
Beide Wege laufen nur mit einem Werkbank-Token (owner_type = dev_agent). Der
Testlauf hatte keines - es gibt keine API, so eines anzulegen, und in der
Produktion geschieht das von Hand. Ein Tokentyp, den kein Testfall benutzt,
ist ein Tokentyp, dessen Sonderwege niemand prueft.
testctl.php kann jetzt action=workbench_token; vier Testfaelle decken die Wege
ab, samt Gegenprobe, dass der Rueckfall aufs Heimatprojekt das Lesen ueber alle
Projekte nicht einschraenkt. 128 Faelle, alle gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1d5ac92051
commit
8b6d6a307e
+2
-2
@@ -15,7 +15,7 @@ Monitoring und einen Bugtracker, den Coding-Agenten selbständig bedienen.
|
|||||||
| **[Änderungen](../public/docs/changelog.json)** | Was sich je Fassung geändert hat und was zu tun ist. Öffentlich unter `/docs/changelog.php`, maschinenlesbar über `GET /api/updateservice/v1/changelog?since=X` |
|
| **[Änderungen](../public/docs/changelog.json)** | Was sich je Fassung geändert hat und was zu tun ist. Öffentlich unter `/docs/changelog.php`, maschinenlesbar über `GET /api/updateservice/v1/changelog?since=X` |
|
||||||
| **[Release-Anleitung für Agenten](../public/docs/release.md)** | Ein Projekt veröffentlichungsfähig machen: Vorlage kopieren, konfigurieren, ausliefern |
|
| **[Release-Anleitung für Agenten](../public/docs/release.md)** | Ein Projekt veröffentlichungsfähig machen: Vorlage kopieren, konfigurieren, ausliefern |
|
||||||
| **[Linux-Testumgebung](./TESTUMGEBUNG.md)** | **Die geteilte Test-VM.** Wie ein Agent dort selbständig testet: DEV-Lizenz holen, Auslieferung prüfen, Befunde melden — mit `dc-testkit` |
|
| **[Linux-Testumgebung](./TESTUMGEBUNG.md)** | **Die geteilte Test-VM.** Wie ein Agent dort selbständig testet: DEV-Lizenz holen, Auslieferung prüfen, Befunde melden — mit `dc-testkit` |
|
||||||
| **[Testinstanz & automatisierter Testlauf](./TESTINSTANZ.md)** | **`dc-dev.mhdf.de` — die zweite Installation zum Kaputtmachen.** 124 Fälle gegen API, Rechte, Migrationen und Zugangssperre (`tests/e2e/run.py`). **`scripts/deploy.py` führt sie vor jedem Produktiv-Upload aus** und bricht bei Fehlschlägen ab (`--ohne-test` umgeht das) |
|
| **[Testinstanz & automatisierter Testlauf](./TESTINSTANZ.md)** | **`dc-dev.mhdf.de` — die zweite Installation zum Kaputtmachen.** 128 Fälle gegen API, Rechte, Migrationen und Zugangssperre (`tests/e2e/run.py`). **`scripts/deploy.py` führt sie vor jedem Produktiv-Upload aus** und bricht bei Fehlschlägen ab (`--ohne-test` umgeht das) |
|
||||||
| [Agent-Prompt-Vorlage](./AGENT_PROMPT_TEMPLATE.md) | Textbaustein für `CLAUDE.md` / `AGENTS.md` eines Projekts |
|
| [Agent-Prompt-Vorlage](./AGENT_PROMPT_TEMPLATE.md) | Textbaustein für `CLAUDE.md` / `AGENTS.md` eines Projekts |
|
||||||
| [Agenten-Handbuch](../public/docs/bugtracker.md) | Vollständige Beschreibung des Bugtracker-Workflows, öffentlich unter `/docs/` |
|
| [Agenten-Handbuch](../public/docs/bugtracker.md) | Vollständige Beschreibung des Bugtracker-Workflows, öffentlich unter `/docs/` |
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ Monitoring und einen Bugtracker, den Coding-Agenten selbständig bedienen.
|
|||||||
- **[Erstinstallation](./SETUP_INTEGRATION_GUIDE.md)** — `setup.json`, Installationskonto, `update-agent --action install`
|
- **[Erstinstallation](./SETUP_INTEGRATION_GUIDE.md)** — `setup.json`, Installationskonto, `update-agent --action install`
|
||||||
- **[Bugtracker](./BUGTRACKER_INTEGRATION_GUIDE.md)** — Anbindung aus Anwendungen heraus
|
- **[Bugtracker](./BUGTRACKER_INTEGRATION_GUIDE.md)** — Anbindung aus Anwendungen heraus
|
||||||
- **[Supervisor](./SUPERVISOR_INTEGRATION_GUIDE.md)** — Befunde: was ist ungewöhnlich? Läuft serverseitig auf ohnehin eingehenden Daten, **ohne Einbau in die Anwendungen**. Enthält auch den Rückbau begonnener Vorbereitungen
|
- **[Supervisor](./SUPERVISOR_INTEGRATION_GUIDE.md)** — Befunde: was ist ungewöhnlich? Läuft serverseitig auf ohnehin eingehenden Daten, **ohne Einbau in die Anwendungen**. Enthält auch den Rückbau begonnener Vorbereitungen
|
||||||
- **[MCP-Adapter](../mcp/README.md)** — Claude Code und andere Werkzeuge direkt an Bugtracker, Roadmap und Supervisor anschließen
|
- **[MCP-Adapter](../mcp/README.md)** — Claude Code **und Claude Desktop** an Bugtracker, Roadmap und Supervisor anschließen — 15 Werkzeuge, 4 Ressourcen, 4 Prompts
|
||||||
|
|
||||||
Hintergrund zu Supervisor und MCP, inklusive der Frage, warum der Client keinen
|
Hintergrund zu Supervisor und MCP, inklusive der Frage, warum der Client keinen
|
||||||
Quelltext sieht: **[archiv/SUPERVISOR_MCP_DESIGN.md](./archiv/SUPERVISOR_MCP_DESIGN.md)**
|
Quelltext sieht: **[archiv/SUPERVISOR_MCP_DESIGN.md](./archiv/SUPERVISOR_MCP_DESIGN.md)**
|
||||||
|
|||||||
+78
-1
@@ -34,7 +34,7 @@ dieselbe, liesse sich nicht mehr sagen, wer was gemeldet hat — und das Signal
|
|||||||
"drei Projekte melden dasselbe" waere wertlos. Also **je Projekt ein eigenes
|
"drei Projekte melden dasselbe" waere wertlos. Also **je Projekt ein eigenes
|
||||||
Token**.
|
Token**.
|
||||||
|
|
||||||
**2. In `.mcp.json` des Projekts eintragen:**
|
**2a. Claude Code — in `.mcp.json` des Projekts:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -48,11 +48,54 @@ Token**.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**2b. Claude Desktop — in `claude_desktop_config.json`:**
|
||||||
|
|
||||||
|
Die Datei liegt unter Windows in `%APPDATA%\Claude\`, unter macOS in
|
||||||
|
`~/Library/Application Support/Claude/`. Gibt es sie noch nicht, anlegen.
|
||||||
|
Danach **Claude Desktop vollständig beenden und neu starten** — die
|
||||||
|
Konfiguration wird nur beim Start gelesen, ein Fensterschließen genügt nicht.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"deploymentcenter": {
|
||||||
|
"command": "node",
|
||||||
|
"args": ["J:\\Softwareprojekte\\Deploymentcenter\\mcp\\deploymentcenter-mcp.mjs"],
|
||||||
|
"env": {
|
||||||
|
"DC_TOKEN": "dc_...",
|
||||||
|
"DC_PROJECT": "deploymentcenter"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Zwei Unterschiede zu Claude Code, beide wichtig:
|
||||||
|
|
||||||
|
- **Absoluter Pfad, Backslashes verdoppelt.** Desktop startet den Prozess ohne
|
||||||
|
Projektverzeichnis; ein relativer Pfad findet nichts. In JSON ist `\` ein
|
||||||
|
Fluchtzeichen, `C:\pfad` also `C:\\pfad`. Vorwärtsschrägstriche gehen unter
|
||||||
|
Windows ebenso und ersparen das Verdoppeln.
|
||||||
|
- **`DC_PROJECT` setzen.** Desktop arbeitet nicht in einem Projekt. Die Angabe
|
||||||
|
bestimmt, auf welches Projekt die Ressourcen zeigen. Ohne sie nehmen sie das
|
||||||
|
Heimatprojekt des Tokens — was auch in Ordnung ist, solange man nur eines
|
||||||
|
betrachtet.
|
||||||
|
|
||||||
|
Findet Desktop `node` nicht, den vollen Pfad eintragen (`where node` unter
|
||||||
|
Windows, `which node` sonst). `command: "node"` setzt voraus, dass Node im PATH
|
||||||
|
des Anmeldedienstes steht, und das ist nicht überall so.
|
||||||
|
|
||||||
|
**Prüfen:** Nach dem Neustart erscheinen unten im Eingabefeld die vier
|
||||||
|
Schrägstrich-Befehle (`arbeit-aufnehmen`, `befunde-sichten`, `fehler-melden`,
|
||||||
|
`roadmap-pflegen`) und im Anhang-Menü die vier Ressourcen. Fehlt beides, steht
|
||||||
|
der Grund im Desktop-Log: `%APPDATA%\Claude\logs\mcp-server-deploymentcenter.log`.
|
||||||
|
|
||||||
Umgebungsvariablen:
|
Umgebungsvariablen:
|
||||||
|
|
||||||
| Variable | Vorgabe | Zweck |
|
| Variable | Vorgabe | Zweck |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `DC_TOKEN` | — | Pflicht. Werkbank-Token. |
|
| `DC_TOKEN` | — | Pflicht. Werkbank-Token. |
|
||||||
|
| `DC_PROJECT` | Heimatprojekt des Tokens | Auf welches Projekt die Ressourcen zeigen. Für Desktop empfohlen. |
|
||||||
| `DC_BASE_URL` | `https://dc.mhdf.de` | Andere Instanz oder lokale Entwicklung. |
|
| `DC_BASE_URL` | `https://dc.mhdf.de` | Andere Instanz oder lokale Entwicklung. |
|
||||||
| `DC_TIMEOUT_MS` | `15000` | Zeitgrenze je Aufruf. |
|
| `DC_TIMEOUT_MS` | `15000` | Zeitgrenze je Aufruf. |
|
||||||
|
|
||||||
@@ -127,6 +170,40 @@ Endpunkte. Werkzeugdefinitionen liegen dauerhaft im Kontextfenster, anders als
|
|||||||
eine Doku, die man einmal liest — ein aufgeblaehter Adapter waere schlechter als
|
eine Doku, die man einmal liest — ein aufgeblaehter Adapter waere schlechter als
|
||||||
die Prompt-Vorlage, die er ersetzt.
|
die Prompt-Vorlage, die er ersetzt.
|
||||||
|
|
||||||
|
## Ressourcen und Prompts
|
||||||
|
|
||||||
|
Werkzeuge ruft ein Modell von sich aus auf. Ressourcen und Prompts wählt ein
|
||||||
|
**Mensch** — in Claude Desktop über das Anhang-Menü bzw. als
|
||||||
|
Schrägstrich-Befehl. Das ist der Unterschied, der sie lohnend macht.
|
||||||
|
|
||||||
|
**Ressourcen** (lesend, live aus dem Deploymentcenter):
|
||||||
|
|
||||||
|
| URI | Inhalt |
|
||||||
|
|---|---|
|
||||||
|
| `dc://roadmap` | Planungsstand als Markdown — derselbe Block, den `cli/roadmap.php` schreibt |
|
||||||
|
| `dc://naechste-schritte` | Was als Nächstes ansteht und wer gerade woran sitzt |
|
||||||
|
| `dc://projekte` | Projektlandschaft mit Beziehungen |
|
||||||
|
| `dc://befunde` | Offene Befunde je Detektor |
|
||||||
|
|
||||||
|
**Prompts** (tragen die Hausregeln mit, nicht nur den Text):
|
||||||
|
|
||||||
|
| Befehl | Wofür |
|
||||||
|
|---|---|
|
||||||
|
| `arbeit-aufnehmen` | Erst nachsehen, wer schon dransitzt, dann übernehmen |
|
||||||
|
| `befunde-sichten` | Befunde einstufen — echt, Artefakt oder Rauschen |
|
||||||
|
| `fehler-melden` | Ins richtige Projekt melden, mit `evidence` bei fremden |
|
||||||
|
| `roadmap-pflegen` | Einplanen und zurückstellen, mit der Auslöser-Regel |
|
||||||
|
|
||||||
|
Der Wert der Prompts liegt darin, dass die Regeln mitkommen: dass vor dem
|
||||||
|
Anfangen nachgesehen wird, dass eine Fremdmeldung eine Begründung braucht, dass
|
||||||
|
ein verworfenes Vorhaben nicht ohne neues Argument wiederkommt. Ohne sie stehen
|
||||||
|
diese Regeln nur in der Werkzeugbeschreibung — und die liest ein Modell erst,
|
||||||
|
wenn es das Werkzeug schon gewählt hat.
|
||||||
|
|
||||||
|
Ressourcen laufen durch dieselbe Abgrenzung wie Werkzeugergebnisse. Sie sind
|
||||||
|
sogar eher ein Einfallstor: Sie landen ungefragt im Kontext, weil ein Mensch
|
||||||
|
sie angehängt hat.
|
||||||
|
|
||||||
## Was der Agent darf und was nicht
|
## Was der Agent darf und was nicht
|
||||||
|
|
||||||
| | eigenes Projekt | fremdes Projekt |
|
| | eigenes Projekt | fremdes Projekt |
|
||||||
|
|||||||
@@ -34,9 +34,33 @@ const BASE_URL = (process.env.DC_BASE_URL || 'https://dc.mhdf.de').replace(/\/+$
|
|||||||
const TOKEN = process.env.DC_TOKEN || '';
|
const TOKEN = process.env.DC_TOKEN || '';
|
||||||
const TIMEOUT_MS = Number(process.env.DC_TIMEOUT_MS || 15000);
|
const TIMEOUT_MS = Number(process.env.DC_TIMEOUT_MS || 15000);
|
||||||
|
|
||||||
|
// Auf welches Projekt zeigen die Ressourcen?
|
||||||
|
//
|
||||||
|
// Leer heisst: das Heimatprojekt des Tokens - der Normalfall, wenn der Adapter
|
||||||
|
// im Projektverzeichnis laeuft. Zu setzen ist es dort, wo ein Mensch am
|
||||||
|
// Ueberblick sitzt statt an einem Projekt: In Claude Desktop haengt man die
|
||||||
|
// Roadmap eines anderen Projekts an, ohne dafuer das Token zu tauschen.
|
||||||
|
// Werkzeuge nehmen project_slug ohnehin je Aufruf entgegen; Ressourcen haben
|
||||||
|
// keine Argumente, deshalb hier.
|
||||||
|
const PROJECT = process.env.DC_PROJECT || '';
|
||||||
|
|
||||||
const SERVER_NAME = 'deploymentcenter';
|
const SERVER_NAME = 'deploymentcenter';
|
||||||
const SERVER_VERSION = '1.0.0';
|
const SERVER_VERSION = '1.1.0';
|
||||||
const DEFAULT_PROTOCOL = '2024-11-05';
|
|
||||||
|
// Protokollfassungen, die dieser Adapter tatsaechlich bedient - neueste zuerst.
|
||||||
|
//
|
||||||
|
// Zuvor wurde bei "initialize" schlicht zurueckgegeben, was der Client
|
||||||
|
// verlangte. Das ist keine Aushandlung, sondern ein Echo: Ein Client, der
|
||||||
|
// 2025-06-18 anfragt, bekam 2025-06-18 bestaetigt und durfte daraufhin
|
||||||
|
// annehmen, was diese Fassung mitbringt - obwohl der Adapter fuer 2024-11-05
|
||||||
|
// geschrieben war. Fehler dieser Art zeigen sich erst spaeter und an ganz
|
||||||
|
// anderer Stelle.
|
||||||
|
//
|
||||||
|
// Jetzt gilt: Kennt der Adapter die verlangte Fassung, bestaetigt er sie.
|
||||||
|
// Kennt er sie nicht, nennt er seine neueste - und der Client entscheidet,
|
||||||
|
// ob ihm das genuegt. Genau so ist die Aushandlung gedacht.
|
||||||
|
const PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'];
|
||||||
|
const DEFAULT_PROTOCOL = PROTOCOL_VERSIONS[0];
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Vertrauensgrenze
|
// Vertrauensgrenze
|
||||||
@@ -555,6 +579,203 @@ const TOOLS = [
|
|||||||
|
|
||||||
const TOOL_BY_NAME = new Map(TOOLS.map((tool) => [tool.name, tool]));
|
const TOOL_BY_NAME = new Map(TOOLS.map((tool) => [tool.name, tool]));
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Ressourcen
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Werkzeuge muss ein Modell von sich aus aufrufen. Ressourcen haengt ein
|
||||||
|
// MENSCH an - in Claude Desktop ueber das Anhang-Menue. Das ist der
|
||||||
|
// Unterschied, der sie lohnend macht: Wer eine Frage zum Projektstand stellt,
|
||||||
|
// legt die Roadmap daneben, statt zu hoffen, dass das Modell dc_roadmap
|
||||||
|
// aufruft.
|
||||||
|
//
|
||||||
|
// Bewusst wenige und nur lesende. Eine Ressource je Endpunkt waere dieselbe
|
||||||
|
// Aufblaehung, die bei den Werkzeugen schon abgelehnt wurde.
|
||||||
|
const RESOURCES = [
|
||||||
|
{
|
||||||
|
uri: 'dc://roadmap',
|
||||||
|
name: 'Roadmap des Projekts',
|
||||||
|
description:
|
||||||
|
'Der Planungsstand als Markdown, so wie ihn cli/roadmap.php in die '
|
||||||
|
+ 'Projektdatei schreibt: Themenbereiche, Reihenfolge, Zielfassung, wer '
|
||||||
|
+ 'woran sitzt. Der Live-Stand - nicht die Kopie im Repository.',
|
||||||
|
mimeType: 'text/markdown',
|
||||||
|
read: async () => {
|
||||||
|
const antwort = await call('GET', '/api/bugtracker/v1/roadmap.php', {
|
||||||
|
query: { action: 'render', project_slug: PROJECT },
|
||||||
|
});
|
||||||
|
return { mimeType: 'text/markdown', text: String(antwort.markdown ?? '') };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'dc://naechste-schritte',
|
||||||
|
name: 'Als Naechstes dran',
|
||||||
|
description:
|
||||||
|
'Die vorderen Eintraege der Warteschlange samt Bearbeiter. Beantwortet '
|
||||||
|
+ '"woran arbeite ich jetzt" und "sitzt da schon jemand dran".',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
read: async () => {
|
||||||
|
const [naechste, aktiv] = await Promise.all([
|
||||||
|
call('GET', '/api/bugtracker/v1/roadmap.php', { query: { action: 'next', limit: 8, project_slug: PROJECT } }),
|
||||||
|
call('GET', '/api/bugtracker/v1/roadmap.php', { query: { action: 'active', project_slug: PROJECT } }),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
mimeType: 'application/json',
|
||||||
|
text: JSON.stringify({ als_naechstes: naechste.items, in_arbeit: aktiv.items }, null, 2),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'dc://projekte',
|
||||||
|
name: 'Projektlandschaft',
|
||||||
|
description:
|
||||||
|
'Alle Projekte mit Slug, offenen Items und der Beziehung zum eigenen '
|
||||||
|
+ '(uses_sdk, depends_on, shares_code). Noetig, bevor ein Slug geraten wird.',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
read: async () => ({
|
||||||
|
mimeType: 'application/json',
|
||||||
|
text: JSON.stringify(await call('GET', '/api/bugtracker/v1/projects'), null, 2),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'dc://befunde',
|
||||||
|
name: 'Offene Befunde je Detektor',
|
||||||
|
description:
|
||||||
|
'Die Zaehlung des Supervisors als Einstieg in die Sichtung: was ist '
|
||||||
|
+ 'ungewoehnlich, und wie oft.',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
read: async () => ({
|
||||||
|
mimeType: 'application/json',
|
||||||
|
text: JSON.stringify(await call('GET', '/api/supervisor/v1/summary', { query: { project_slug: PROJECT } }), null, 2),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const RESOURCE_BY_URI = new Map(RESOURCES.map((r) => [r.uri, r]));
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Prompts
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// In Claude Desktop erscheinen sie als Schraegstrich-Befehle im Eingabefeld.
|
||||||
|
// Ihr Wert liegt nicht darin, Tipparbeit zu sparen, sondern darin, die
|
||||||
|
// Hausregeln mitzuliefern: dass vor dem Anfangen nachgesehen wird, wer schon
|
||||||
|
// dransitzt; dass eine Fremdmeldung eine Begruendung braucht; dass ein
|
||||||
|
// verworfenes Vorhaben nicht ohne neues Argument wiederkommt.
|
||||||
|
//
|
||||||
|
// Ohne sie stehen diese Regeln nur in der Werkzeugbeschreibung - und die liest
|
||||||
|
// ein Modell erst, wenn es das Werkzeug schon gewaehlt hat.
|
||||||
|
const PROMPTS = [
|
||||||
|
{
|
||||||
|
name: 'arbeit-aufnehmen',
|
||||||
|
description: 'Was ist als Naechstes dran? Planungsstand pruefen, Item uebernehmen, anfangen.',
|
||||||
|
arguments: [],
|
||||||
|
build: () => `Nimm Arbeit im Deploymentcenter auf. Halte dabei diese Reihenfolge ein:
|
||||||
|
|
||||||
|
1. dc_roadmap aufrufen. Es beantwortet zwei Fragen, die man sich sonst falsch
|
||||||
|
beantwortet: was steht an, und sitzt dort schon jemand dran.
|
||||||
|
2. Erst dann dc_next_task, um ein Item exklusiv zu uebernehmen. Ohne diesen
|
||||||
|
Schritt arbeiten zwei Agenten am selben Problem.
|
||||||
|
3. Kommt ein Item mit Status "in_verification" oder "awaiting_verification"
|
||||||
|
zurueck, lautet die Aufgabe NICHT "behebe das", sondern "traegt der Fix?".
|
||||||
|
In resolved_in_build steht, gegen welche Fassung zu pruefen ist.
|
||||||
|
4. Fortschritt mit dc_comment festhalten, solange du arbeitest.
|
||||||
|
5. Kommst du nicht weiter: dc_release. Ein gehaltenes Item sperrt es fuer alle
|
||||||
|
anderen bis zum Lease-Ende.
|
||||||
|
|
||||||
|
Sag mir zuerst, was du vorgefunden hast und was du dir vornimmst - bevor du
|
||||||
|
etwas aenderst.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'befunde-sichten',
|
||||||
|
description: 'Supervisor-Befunde durchgehen und einstufen: echt, Artefakt oder Rauschen?',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: 'detektor',
|
||||||
|
description: 'Nur eine Detektor-Familie, z. B. "deviation" oder "novel". Optional.',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
build: (args) => `Sichte die offenen Supervisor-Befunde${
|
||||||
|
args.detektor ? ` der Familie "${args.detektor}"` : ''
|
||||||
|
}.
|
||||||
|
|
||||||
|
Fang mit dc_finding_summary an - die Zaehlung je Detektor zeigt, wo ueberhaupt
|
||||||
|
etwas los ist. Dann dc_findings fuer die Einzelbefunde, dc_metrics wenn du den
|
||||||
|
Verlauf einer Kennzahl brauchst, um einen Befund zu beurteilen.
|
||||||
|
|
||||||
|
Ein Befund behauptet nicht, dass etwas kaputt ist, sondern dass etwas
|
||||||
|
ungewoehnlich ist. Die Frage "ist das schlimm?" beantwortest du, nicht der
|
||||||
|
Detektor.
|
||||||
|
|
||||||
|
Achte besonders auf die Umgebung: Was ausserhalb von "production" auffiel, ist
|
||||||
|
eine Stufe niedriger einzuordnen - dort entsteht systematisch mehr Rauschen.
|
||||||
|
Aber die Umkehrung gilt auch: Fehlende Zugangsdaten auf dem Pruefstand sind ein
|
||||||
|
Artefakt, WIE die Software darauf reagiert dagegen nicht. Endlose
|
||||||
|
Wiederholungen, verschluckte Fehler, halb geschriebener Zustand oder ein
|
||||||
|
Passwort im Protokoll sind echte Befunde, auch wenn der Ausloeser kuenstlich war.
|
||||||
|
|
||||||
|
Schlag mir eine Einstufung je Befund vor. Setzen tue ich sie, nicht du.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'fehler-melden',
|
||||||
|
description: 'Einen Fehler oder eine Idee im richtigen Projekt melden - auch in einem fremden.',
|
||||||
|
arguments: [
|
||||||
|
{ name: 'beobachtung', description: 'Was ist passiert?', required: true },
|
||||||
|
],
|
||||||
|
build: (args) => `Melde das im Deploymentcenter:
|
||||||
|
|
||||||
|
${args.beobachtung}
|
||||||
|
|
||||||
|
Vorher zu klaeren, in dieser Reihenfolge:
|
||||||
|
|
||||||
|
1. In WELCHEM Projekt liegt die Ursache? Nicht dort melden, wo du gerade
|
||||||
|
arbeitest, sondern wo der Fehler herkommt. Ein Fehler im SDK, im
|
||||||
|
update-agent, in der Lizenzpruefung oder im Bugtracker gehoert unter
|
||||||
|
"deploymentcenter". Bei Zweifeln dc_projects aufrufen - den Slug nicht raten.
|
||||||
|
2. Ist es ein FREMDES Projekt, ist "evidence" Pflicht: was wurde beobachtet,
|
||||||
|
und warum liegt die Ursache dort und nicht bei uns? Pruefe ehrlich, ob nicht
|
||||||
|
das eigene Projekt die Schnittstelle falsch benutzt - das ist der haeufigere
|
||||||
|
Fall.
|
||||||
|
3. Mit dc_search nachsehen, ob es das schon gibt. Ein zweites Item zur selben
|
||||||
|
Sache verteilt die Diskussion auf zwei Straenge.
|
||||||
|
4. client_ref setzen, damit ein wiederholter Aufruf kein Duplikat anlegt, und
|
||||||
|
build_version mitschicken - daran entscheidet der Server, ob es eine
|
||||||
|
Regression ist oder nur ein veralteter Pruefstand.
|
||||||
|
|
||||||
|
Zeig mir den Entwurf, bevor du dc_report aufrufst.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'roadmap-pflegen',
|
||||||
|
description: 'Ein Vorhaben einplanen, umsortieren oder zuruecksetzen - mit den Regeln dazu.',
|
||||||
|
arguments: [
|
||||||
|
{ name: 'vorhaben', description: 'Worum geht es?', required: false },
|
||||||
|
],
|
||||||
|
build: (args) => `Pflege die Roadmap im Deploymentcenter${
|
||||||
|
args.vorhaben ? `. Es geht um: ${args.vorhaben}` : ''
|
||||||
|
}.
|
||||||
|
|
||||||
|
Der aktuelle Stand kommt aus dc_roadmap, geaendert wird mit dc_roadmap_set.
|
||||||
|
Vier Regeln, die dabei gelten:
|
||||||
|
|
||||||
|
1. Ein Vorhaben steht auf der Roadmap, sobald es einen Themenbereich (epic)
|
||||||
|
hat. Das ist das einzige Kennzeichen. Erst dc_report anlegen, dann
|
||||||
|
einplanen - anlegen und einplanen sind zwei Entscheidungen.
|
||||||
|
2. Die Reihenfolge laeuft in Zehnerschritten. Die Luecken sind Absicht: So
|
||||||
|
laesst sich spaeter etwas dazwischenschieben, ohne alles neu zu nummerieren.
|
||||||
|
3. Zurueckstellen geht nur mit defer_trigger - woran erkennt man, dass
|
||||||
|
"spaeter" jetzt ist? Ohne benannten Anlass ist ein Vorhaben nicht
|
||||||
|
zurueckgestellt, sondern vergessen. Der Server lehnt es sonst ab.
|
||||||
|
4. Steht etwas auf "rejected" oder "deferred", lies erst die Begruendung, auf
|
||||||
|
die doc_anchor zeigt, bevor du es erneut vorschlaegst. Ein neuer Vorschlag
|
||||||
|
braucht ein Argument, das dort noch nicht steht.
|
||||||
|
|
||||||
|
Der Zustand gehoert ins Deploymentcenter, die Begruendung in die Projektdatei.
|
||||||
|
Trag keine Statuszeilen von Hand in eine Markdown-Datei - der erzeugte Block
|
||||||
|
zwischen den dc:roadmap-Markern wird beim naechsten Lauf ueberschrieben.`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const PROMPT_BY_NAME = new Map(PROMPTS.map((p) => [p.name, p]));
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// JSON-RPC ueber stdio
|
// JSON-RPC ueber stdio
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -628,19 +849,99 @@ async function handleToolCall(id, params) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liefert den Inhalt einer Ressource.
|
||||||
|
*
|
||||||
|
* Anders als bei Werkzeugen gibt es hier kein isError-Feld - ein Fehlschlag
|
||||||
|
* ist ein JSON-RPC-Fehler. Sonst haengte ein Client eine Fehlermeldung als
|
||||||
|
* Kontext an und arbeitete damit weiter, als waere es die Roadmap.
|
||||||
|
*/
|
||||||
|
async function handleResourceRead(id, params) {
|
||||||
|
const uri = params?.uri;
|
||||||
|
const resource = RESOURCE_BY_URI.get(uri);
|
||||||
|
|
||||||
|
if (!resource) {
|
||||||
|
replyError(id, -32602, `Unbekannte Ressource "${uri}".`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { mimeType, text } = await resource.read();
|
||||||
|
|
||||||
|
// Dieselbe Vertrauensgrenze wie bei den Werkzeugen. Eine Ressource wird
|
||||||
|
// sogar eher zum Einfallstor: Sie landet ungefragt im Kontext, weil ein
|
||||||
|
// Mensch sie angehaengt hat - und in Titeln und Fehlertexten steht, was
|
||||||
|
// auf fremden Maschinen entstanden ist.
|
||||||
|
const inhalt = mimeType === 'application/json'
|
||||||
|
? JSON.stringify(guard(JSON.parse(text)), null, 2)
|
||||||
|
: text;
|
||||||
|
|
||||||
|
const hinweis = inhalt.includes('<<<FREMDDATEN') ? `${TRUST_NOTE}\n\n` : '';
|
||||||
|
|
||||||
|
reply(id, { contents: [{ uri, mimeType, text: hinweis + inhalt }] });
|
||||||
|
} catch (err) {
|
||||||
|
replyError(
|
||||||
|
id,
|
||||||
|
-32603,
|
||||||
|
err instanceof ToolError
|
||||||
|
? `${err.code}: ${err.message}`
|
||||||
|
: `Ressource "${uri}" nicht lesbar: ${err.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Baut einen Prompt zusammen. Rein oertlich - kein Aufruf ins Deploymentcenter. */
|
||||||
|
function handlePromptGet(id, params) {
|
||||||
|
const prompt = PROMPT_BY_NAME.get(params?.name);
|
||||||
|
|
||||||
|
if (!prompt) {
|
||||||
|
replyError(id, -32602, `Unbekannter Prompt "${params?.name}".`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = params?.arguments ?? {};
|
||||||
|
|
||||||
|
for (const feld of prompt.arguments) {
|
||||||
|
if (feld.required && !args[feld.name]) {
|
||||||
|
replyError(id, -32602, `Dem Prompt "${prompt.name}" fehlt "${feld.name}".`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Argumente kommen vom Menschen im Eingabefeld, nicht von einer fremden
|
||||||
|
// Maschine - sie werden deshalb nicht abgegrenzt. Eingesetzt werden sie
|
||||||
|
// trotzdem nur an den vorgesehenen Stellen und nie als Anweisung.
|
||||||
|
reply(id, {
|
||||||
|
description: prompt.description,
|
||||||
|
messages: [{ role: 'user', content: { type: 'text', text: prompt.build(args) } }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function handle(message) {
|
async function handle(message) {
|
||||||
const { id, method, params } = message;
|
const { id, method, params } = message;
|
||||||
|
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case 'initialize':
|
case 'initialize': {
|
||||||
|
// Aushandeln statt spiegeln: Nur eine Fassung bestaetigen, die dieser
|
||||||
|
// Adapter auch bedient. Kennt er die verlangte nicht, nennt er seine
|
||||||
|
// neueste und ueberlaesst dem Client die Entscheidung.
|
||||||
|
const verlangt = params?.protocolVersion;
|
||||||
|
const version = PROTOCOL_VERSIONS.includes(verlangt) ? verlangt : DEFAULT_PROTOCOL;
|
||||||
|
|
||||||
reply(id, {
|
reply(id, {
|
||||||
protocolVersion: typeof params?.protocolVersion === 'string'
|
protocolVersion: version,
|
||||||
? params.protocolVersion
|
capabilities: {
|
||||||
: DEFAULT_PROTOCOL,
|
tools: {},
|
||||||
capabilities: { tools: {} },
|
// listChanged bleibt aus: Werkzeuge, Ressourcen und Prompts stehen
|
||||||
|
// fest, solange der Prozess laeuft. Die Zusage zu geben und nie
|
||||||
|
// etwas zu senden waere schlechter als sie zu lassen.
|
||||||
|
resources: {},
|
||||||
|
prompts: {},
|
||||||
|
},
|
||||||
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
|
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
case 'notifications/initialized':
|
case 'notifications/initialized':
|
||||||
case 'notifications/cancelled':
|
case 'notifications/cancelled':
|
||||||
@@ -665,6 +966,37 @@ async function handle(message) {
|
|||||||
await handleToolCall(id, params);
|
await handleToolCall(id, params);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
case 'resources/list':
|
||||||
|
reply(id, {
|
||||||
|
resources: RESOURCES.map(({ uri, name, description, mimeType }) => ({
|
||||||
|
uri, name, description, mimeType,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Es gibt keine Vorlagen mit Platzhaltern - die Antwort ist leer und nicht
|
||||||
|
// "unbekannte Methode". Ein Client, der beim Aufbau der Anhangsliste einen
|
||||||
|
// -32601 bekommt, meldet das als Fehler, obwohl nichts falsch ist.
|
||||||
|
case 'resources/templates/list':
|
||||||
|
reply(id, { resourceTemplates: [] });
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'resources/read':
|
||||||
|
await handleResourceRead(id, params);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'prompts/list':
|
||||||
|
reply(id, {
|
||||||
|
prompts: PROMPTS.map(({ name, description, arguments: args }) => ({
|
||||||
|
name, description, arguments: args,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'prompts/get':
|
||||||
|
handlePromptGet(id, params);
|
||||||
|
return;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if (id !== undefined) replyError(id, -32601, `Unbekannte Methode "${method}".`);
|
if (id !== undefined) replyError(id, -32601, `Unbekannte Methode "${method}".`);
|
||||||
}
|
}
|
||||||
@@ -679,7 +1011,25 @@ let pending = 0;
|
|||||||
let closed = false;
|
let closed = false;
|
||||||
|
|
||||||
function drained() {
|
function drained() {
|
||||||
if (closed && pending === 0) process.exit(0);
|
if (!closed || pending > 0) return;
|
||||||
|
|
||||||
|
// NICHT process.exit(). Der reisst den Prozess sofort ab, auch wenn libuv
|
||||||
|
// gerade dabei ist, Handles zu schliessen - unter Windows quittiert das die
|
||||||
|
// Laufzeit mit
|
||||||
|
//
|
||||||
|
// Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), src\win\async.c
|
||||||
|
//
|
||||||
|
// und einem Rueckgabewert ungleich null. Fuer den Aufrufer sieht ein
|
||||||
|
// ordentlich beendeter Adapter dann aus wie ein abgestuerzter; Claude
|
||||||
|
// Desktop meldet den Server als fehlgeschlagen, obwohl jede Antwort
|
||||||
|
// rausgegangen ist.
|
||||||
|
//
|
||||||
|
// Stattdessen: Rueckgabewert vormerken und die letzte Referenz auf stdin
|
||||||
|
// loesen. Node beendet sich, sobald nichts mehr in der Schleife haengt -
|
||||||
|
// also nachdem geschrieben und geschlossen wurde.
|
||||||
|
process.exitCode = 0;
|
||||||
|
input.close();
|
||||||
|
process.stdin.unref();
|
||||||
}
|
}
|
||||||
|
|
||||||
input.on('line', (line) => {
|
input.on('line', (line) => {
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ switch ($action) {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
case 'render': {
|
case 'render': {
|
||||||
$project = requireProject($scope);
|
$project = requireProject($scope, $context);
|
||||||
Http::ok([
|
Http::ok([
|
||||||
'project_slug' => $project,
|
'project_slug' => $project,
|
||||||
'markdown' => $roadmap->renderBlock($project),
|
'markdown' => $roadmap->renderBlock($project),
|
||||||
@@ -100,7 +100,7 @@ switch ($action) {
|
|||||||
// dass der Server ihn liest: Die Datei liegt im Repository des
|
// dass der Server ihn liest: Die Datei liegt im Repository des
|
||||||
// Projekts, oft auf einer anderen Maschine. Der Server kennt sie nicht
|
// Projekts, oft auf einer anderen Maschine. Der Server kennt sie nicht
|
||||||
// und soll auch nicht anfangen, in Verzeichnissen zu suchen.
|
// und soll auch nicht anfangen, in Verzeichnissen zu suchen.
|
||||||
$project = requireProject($scope);
|
$project = requireProject($scope, $context);
|
||||||
$content = Http::str('content');
|
$content = Http::str('content');
|
||||||
|
|
||||||
if ($content === null) {
|
if ($content === null) {
|
||||||
@@ -218,7 +218,7 @@ switch ($action) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'reorder': {
|
case 'reorder': {
|
||||||
$project = requireProject($writeProject ?? $requested);
|
$project = requireProject($writeProject ?? $requested, $context);
|
||||||
$epic = Http::str('epic');
|
$epic = Http::str('epic');
|
||||||
$ids = Http::input('ids');
|
$ids = Http::input('ids');
|
||||||
|
|
||||||
@@ -235,7 +235,7 @@ switch ($action) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'epic': {
|
case 'epic': {
|
||||||
$project = requireProject($writeProject ?? $requested);
|
$project = requireProject($writeProject ?? $requested, $context);
|
||||||
$epic = Http::str('epic');
|
$epic = Http::str('epic');
|
||||||
$title = Http::str('title');
|
$title = Http::str('title');
|
||||||
|
|
||||||
@@ -260,14 +260,28 @@ switch ($action) {
|
|||||||
*
|
*
|
||||||
* "all" ist beim Lesen sinnvoll und beim Schreiben oder Erzeugen nicht - eine
|
* "all" ist beim Lesen sinnvoll und beim Schreiben oder Erzeugen nicht - eine
|
||||||
* Datei gehoert zu genau einem Repository.
|
* Datei gehoert zu genau einem Repository.
|
||||||
|
*
|
||||||
|
* Der Rueckfall auf das Heimatprojekt ist nicht Bequemlichkeit, sondern
|
||||||
|
* behebt einen Widerspruch: Beim Lesen laesst projectFilter() ein
|
||||||
|
* Werkbank-Token bewusst durch (es soll die Landschaft sehen), liefert also
|
||||||
|
* null - und damit wurde $scope zu "all". Ausgerechnet der Tokentyp, den der
|
||||||
|
* MCP-Adapter verlangt, konnte seine eigene Roadmap nicht rendern.
|
||||||
|
*
|
||||||
|
* Das Token ist trotzdem an ein Projekt gebunden; diese Bindung liefert
|
||||||
|
* homeProject(). "Meine Roadmap" ohne Angabe heisst genau das.
|
||||||
*/
|
*/
|
||||||
function requireProject(?string $slug): string
|
function requireProject(?string $slug, array $context = []): string
|
||||||
{
|
{
|
||||||
if ($slug === null || $slug === '' || $slug === 'all') {
|
if ($slug !== null && $slug !== '' && $slug !== 'all') {
|
||||||
Http::fail(400, 'missing_project', 'Diese Aktion braucht ein konkretes "project_slug".');
|
return $slug;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $slug;
|
$home = ApiAuth::homeProject($context);
|
||||||
|
if ($home !== null) {
|
||||||
|
return $home;
|
||||||
|
}
|
||||||
|
|
||||||
|
Http::fail(400, 'missing_project', 'Diese Aktion braucht ein konkretes "project_slug".');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -64,6 +64,34 @@
|
|||||||
"title": "Testinstanz dc-dev.mhdf.de und ein Testtor vor jedem Produktiv-Upload",
|
"title": "Testinstanz dc-dev.mhdf.de und ein Testtor vor jedem Produktiv-Upload",
|
||||||
"text": "Eine vollstaendige zweite Installation, die jederzeit geloescht und neu aufgebaut werden darf: eigene Datenbank, eigene Schluessel, Basic-Auth davor, Rocket.Chat aus. Dazu 124 Testfaelle in tests/e2e gegen API, Rechte, Migrationen und die Zugangssperre. scripts/deploy.py bringt vor jedem Upload erst den anstehenden Stand auf die Testinstanz und laesst dann den Testlauf darauf laufen; erst bei Rueckgabewert 0 geht etwas auf die Produktion (--ohne-test ist der Notausgang). Die Reihenfolge ist der Punkt: Liefe der Test gegen den vorhandenen Dev-Stand, prueft er aelteren Code als den, der gleich hochgeht. Gefundene, noch offene Fehler stehen mit Item-Nummer in tests/e2e/bekannte_befunde.json - sie laufen als BEK mit und blockieren nicht; besteht so ein Fall wieder, sagt der Lauf das ausdruecklich. Ein Lauf, in dem mehr uebersprungen als geprueft wurde, endet mit Rueckgabewert 2 statt 0: nichts ist kaputt, geprueft wurde es aber auch nicht, und der Unterschied darf nicht am Rueckgabewert verschwinden. Anleitung: docs/TESTINSTANZ.md.",
|
"text": "Eine vollstaendige zweite Installation, die jederzeit geloescht und neu aufgebaut werden darf: eigene Datenbank, eigene Schluessel, Basic-Auth davor, Rocket.Chat aus. Dazu 124 Testfaelle in tests/e2e gegen API, Rechte, Migrationen und die Zugangssperre. scripts/deploy.py bringt vor jedem Upload erst den anstehenden Stand auf die Testinstanz und laesst dann den Testlauf darauf laufen; erst bei Rueckgabewert 0 geht etwas auf die Produktion (--ohne-test ist der Notausgang). Die Reihenfolge ist der Punkt: Liefe der Test gegen den vorhandenen Dev-Stand, prueft er aelteren Code als den, der gleich hochgeht. Gefundene, noch offene Fehler stehen mit Item-Nummer in tests/e2e/bekannte_befunde.json - sie laufen als BEK mit und blockieren nicht; besteht so ein Fall wieder, sagt der Lauf das ausdruecklich. Ein Lauf, in dem mehr uebersprungen als geprueft wurde, endet mit Rueckgabewert 2 statt 0: nichts ist kaputt, geprueft wurde es aber auch nicht, und der Unterschied darf nicht am Rueckgabewert verschwinden. Anleitung: docs/TESTINSTANZ.md.",
|
||||||
"action": "Wer deployt, braucht scripts/deploy_dev_config.json (Vorlage: ...example.json). Ohne sie bricht scripts/deploy.py ab und verweist auf --ohne-test."
|
"action": "Wer deployt, braucht scripts/deploy_dev_config.json (Vorlage: ...example.json). Ohne sie bricht scripts/deploy.py ab und verweist auf --ohne-test."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"area": "mcp",
|
||||||
|
"breaking": false,
|
||||||
|
"title": "MCP-Adapter 1.1: Ressourcen, Prompts und eine echte Protokollaushandlung",
|
||||||
|
"text": "Werkzeuge ruft ein Modell von sich aus auf - Ressourcen und Prompts waehlt ein MENSCH. In Claude Desktop sind das das Anhang-Menue und die Schraegstrich-Befehle, und genau die fehlten. Neu: vier Ressourcen (dc://roadmap als fertiger Markdown-Block, dc://naechste-schritte, dc://projekte, dc://befunde) und vier Prompts (arbeit-aufnehmen, befunde-sichten, fehler-melden, roadmap-pflegen). Der Wert der Prompts liegt darin, dass die Hausregeln mitkommen: erst nachsehen wer dransitzt, Fremdmeldung nur mit Begruendung, verworfene Vorhaben nicht ohne neues Argument wiedervorschlagen. Bisher standen die nur in Werkzeugbeschreibungen - und die liest ein Modell erst, wenn es das Werkzeug schon gewaehlt hat. Ressourcen laufen durch dieselbe Fremddaten-Abgrenzung wie Werkzeugergebnisse; sie sind sogar eher ein Einfallstor, weil sie ungefragt im Kontext landen. BEHOBEN dabei: \"initialize\" spiegelte die vom Client verlangte Protokollfassung einfach zurueck. Ein Client, der 2025-06-18 anfragte, bekam sie bestaetigt, obwohl der Adapter fuer 2024-11-05 geschrieben war. Jetzt wird gegen eine Liste tatsaechlich bedienter Fassungen ausgehandelt. Ebenfalls behoben: Beim Beenden rief der Adapter process.exit() mitten in den schliessenden libuv-Handles - unter Windows eine Assertion und ein Rueckgabewert ungleich null, also ein sauber beendeter Server, der wie ein abgestuerzter aussah.",
|
||||||
|
"action": "Fuer Claude Desktop: Eintrag in claude_desktop_config.json (%APPDATA%\\Claude\\ bzw. ~/Library/Application Support/Claude/), absoluter Pfad, DC_PROJECT setzen, Desktop vollstaendig neu starten. Anleitung in mcp/README.md."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"area": "api",
|
||||||
|
"breaking": false,
|
||||||
|
"title": "Behoben: Werkbank-Token konnte seine eigene Roadmap nicht rendern",
|
||||||
|
"text": "roadmap.php?action=render und ?action=check verlangen ein konkretes Projekt. Beim Lesen laesst projectFilter() ein Werkbank-Token aber bewusst durch - es soll die ganze Landschaft sehen - und liefert deshalb null. Damit wurde der Projektbezug zu \"all\" und der Aufruf mit 400 missing_project abgelehnt. Betroffen war ausgerechnet der Tokentyp, den der MCP-Adapter und dc-testkit verlangen. Das Token ist trotzdem an ein Projekt gebunden; dieser Bezug dient jetzt als Rueckfall. \"Meine Roadmap\" ohne Angabe heisst genau das.",
|
||||||
|
"action": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"area": "api",
|
||||||
|
"breaking": false,
|
||||||
|
"title": "Behoben: Projektliste warf 500 fuer Werkbank-Token (HY093)",
|
||||||
|
"text": "ProjectLinks::between() benutzte :a dreimal und :b zweimal im selben Statement, forProject() :slug zweimal. Bei ATTR_EMULATE_PREPARES = false ist das unzulaessig - PDO reicht die Namen an den Server durch, und der zaehlt Parameter, statt sie zu benennen. Ergebnis: SQLSTATE[HY093] und ein 500 auf /api/bugtracker/v1/projects. Die Abfrage laeuft nur, wenn ein Heimatprojekt gesetzt ist, also nur mit einem Werkbank-Token - fuer das Master-Token des Testlaufs wurde der Zweig uebersprungen. Derselbe Fehler steckte schon einmal in der Volltextsuche des Bugtrackers; ein Suchlauf ueber alle prepare()-Aufrufe der Codebasis fand jetzt keine weitere Stelle.",
|
||||||
|
"action": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"area": "betrieb",
|
||||||
|
"breaking": false,
|
||||||
|
"title": "Testlauf kennt jetzt den Werkbank-Token - der Tokentyp, den die Agenten benutzen",
|
||||||
|
"text": "Es gibt keine API, ein dev_agent-Token anzulegen; in der Produktion geschieht das von Hand in der Token-Verwaltung, und das bleibt auch so. Fuer den Testlauf fehlte damit ausgerechnet der Tokentyp, den MCP-Adapter und dc-testkit verwenden - mit der Folge, dass seine Sonderwege niemand prueft. Genau dort steckten die beiden Befunde oben. testctl.php kann jetzt action=workbench_token, und vier Testfaelle decken die Wege ab: Roadmap ohne Projektangabe rendern, Projektliste mit Beziehungen, und die Gegenprobe, dass der Rueckfall aufs Heimatprojekt das Lesen ueber alle Projekte nicht einschraenkt. Der Testlauf steht damit bei 128 Faellen.",
|
||||||
|
"action": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -43,13 +43,16 @@ final class ProjectLinks
|
|||||||
*/
|
*/
|
||||||
public function forProject(string $slug): array
|
public function forProject(string $slug): array
|
||||||
{
|
{
|
||||||
|
// Zwei Platzhalter fuer denselben Wert - aus demselben Grund wie in
|
||||||
|
// between(): ein Name darf bei nativen Prepared Statements nur einmal
|
||||||
|
// vorkommen. Diese Abfrage haette beim ersten Aufruf HY093 geworfen.
|
||||||
$stmt = $this->db->prepare('
|
$stmt = $this->db->prepare('
|
||||||
SELECT from_slug, to_slug, kind, integrated_version, integrated_at, notes
|
SELECT from_slug, to_slug, kind, integrated_version, integrated_at, notes
|
||||||
FROM dc_project_links
|
FROM dc_project_links
|
||||||
WHERE from_slug = :slug OR to_slug = :slug
|
WHERE from_slug = :slug_from OR to_slug = :slug_to
|
||||||
ORDER BY kind ASC, to_slug ASC
|
ORDER BY kind ASC, to_slug ASC
|
||||||
');
|
');
|
||||||
$stmt->execute([':slug' => $slug]);
|
$stmt->execute([':slug_from' => $slug, ':slug_to' => $slug]);
|
||||||
|
|
||||||
$uses = [];
|
$uses = [];
|
||||||
$usedBy = [];
|
$usedBy = [];
|
||||||
@@ -86,15 +89,31 @@ final class ProjectLinks
|
|||||||
return ['direction' => 'self', 'kind' => 'self', 'integrated_version' => null];
|
return ['direction' => 'self', 'kind' => 'self', 'integrated_version' => null];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Jedes Vorkommen bekommt einen eigenen Platzhalter.
|
||||||
|
//
|
||||||
|
// :a stand dreimal und :b zweimal im Statement. Bei
|
||||||
|
// ATTR_EMULATE_PREPARES = false ist das nicht zulaessig - PDO reicht
|
||||||
|
// die Namen an den Server durch, und der zaehlt Parameter, statt sie
|
||||||
|
// zu benennen. Ergebnis: HY093 "Invalid parameter number", also ein
|
||||||
|
// 500 statt einer Antwort.
|
||||||
|
//
|
||||||
|
// Derselbe Fehler steckte schon einmal in der Volltextsuche des
|
||||||
|
// Bugtrackers (siehe Kopfkommentar von BugRepo). Er faellt deshalb so
|
||||||
|
// spaet auf, weil er erst bei der Ausfuehrung zuschlaegt und diese
|
||||||
|
// Abfrage nur laeuft, wenn ein Heimatprojekt gesetzt ist - also nur
|
||||||
|
// mit einem Werkbank-Token.
|
||||||
$stmt = $this->db->prepare('
|
$stmt = $this->db->prepare('
|
||||||
SELECT from_slug, kind, integrated_version
|
SELECT from_slug, kind, integrated_version
|
||||||
FROM dc_project_links
|
FROM dc_project_links
|
||||||
WHERE (from_slug = :a AND to_slug = :b)
|
WHERE (from_slug = :a1 AND to_slug = :b1)
|
||||||
OR (from_slug = :b AND to_slug = :a)
|
OR (from_slug = :b2 AND to_slug = :a2)
|
||||||
ORDER BY from_slug = :a DESC
|
ORDER BY from_slug = :a3 DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
');
|
');
|
||||||
$stmt->execute([':a' => $from, ':b' => $to]);
|
$stmt->execute([
|
||||||
|
':a1' => $from, ':a2' => $from, ':a3' => $from,
|
||||||
|
':b1' => $to, ':b2' => $to,
|
||||||
|
]);
|
||||||
$row = $stmt->fetch();
|
$row = $stmt->fetch();
|
||||||
|
|
||||||
if (!is_array($row)) {
|
if (!is_array($row)) {
|
||||||
|
|||||||
@@ -437,6 +437,85 @@ def _(ctx):
|
|||||||
erwarte_erfolg(r)
|
erwarte_erfolg(r)
|
||||||
|
|
||||||
|
|
||||||
|
@test('roadmap', 'Werkbank-Token laesst sich anlegen')
|
||||||
|
def _(ctx):
|
||||||
|
"""
|
||||||
|
Der Tokentyp, den MCP-Adapter und dc-testkit benutzen: dev_agent, an ein
|
||||||
|
Projekt gebunden. Bis hierher benutzte ihn kein Testfall - und genau
|
||||||
|
deshalb blieb der Fall darunter jahrelang ungeprueft.
|
||||||
|
"""
|
||||||
|
r = ctx.testctl('workbench_token', project_slug=PROJEKT, name='e2e-werkbank')
|
||||||
|
erwarte_erfolg(r)
|
||||||
|
erwarte(r.feld('raw_token'), 'Kein Token in der Antwort: %s' % r.kurz(300))
|
||||||
|
ctx['werkbank_token'] = r.feld('raw_token')
|
||||||
|
|
||||||
|
|
||||||
|
@test('roadmap', 'Werkbank-Token rendert seine Roadmap ohne Projektangabe')
|
||||||
|
def _(ctx):
|
||||||
|
"""
|
||||||
|
BEFUND (29.08.2026): Beim Lesen laesst projectFilter() ein Werkbank-Token
|
||||||
|
bewusst durch - es soll die ganze Landschaft sehen und liefert deshalb
|
||||||
|
null. Damit wurde der Projektbezug zu "all", und action=render lehnte mit
|
||||||
|
"missing_project" ab. Ausgerechnet der Tokentyp, den der MCP-Adapter
|
||||||
|
verlangt, konnte seine eigene Roadmap nicht rendern.
|
||||||
|
|
||||||
|
Das Token ist trotzdem an ein Projekt gebunden. "Meine Roadmap" ohne
|
||||||
|
Angabe heisst genau dieses Projekt.
|
||||||
|
"""
|
||||||
|
r = ctx.client.get('/api/bugtracker/v1/roadmap.php',
|
||||||
|
query={'action': 'render'},
|
||||||
|
token=ctx['werkbank_token'])
|
||||||
|
erwarte_erfolg(r)
|
||||||
|
erwarte(r.feld('project_slug') == PROJEKT,
|
||||||
|
'Gerendert wurde %r statt des Heimatprojekts %r'
|
||||||
|
% (r.feld('project_slug'), PROJEKT))
|
||||||
|
erwarte('dc:roadmap' in (r.feld('markdown') or ''),
|
||||||
|
'Im Ergebnis fehlt der Blockmarker: %s' % r.kurz(200))
|
||||||
|
|
||||||
|
|
||||||
|
@test('roadmap', 'Werkbank-Token bekommt die Projektliste mit Beziehungen')
|
||||||
|
def _(ctx):
|
||||||
|
"""
|
||||||
|
BEFUND (29.08.2026): ProjectLinks::between() und forProject() benutzten
|
||||||
|
denselben Platzhalter mehrfach im Statement. Bei
|
||||||
|
ATTR_EMULATE_PREPARES = false ist das nicht zulaessig - PDO zaehlt
|
||||||
|
Parameter, statt sie zu benennen, und wirft HY093.
|
||||||
|
|
||||||
|
Die Abfrage laeuft nur, wenn ein Heimatprojekt gesetzt ist, also
|
||||||
|
ausschliesslich mit einem Werkbank-Token. Fuer das Master-Token des
|
||||||
|
Testlaufs wurde der Zweig uebersprungen - deshalb blieb ein sicherer
|
||||||
|
500er unbemerkt, bis der MCP-Adapter die Ressource dc://projekte abrief.
|
||||||
|
|
||||||
|
Derselbe Fehler steckte schon in der Volltextsuche des Bugtrackers. Beim
|
||||||
|
dritten Mal ist es kein Zufall mehr, sondern eine Falle des Frameworks -
|
||||||
|
ein Suchlauf ueber alle prepare()-Aufrufe gehoert zur Durchsicht.
|
||||||
|
"""
|
||||||
|
r = ctx.client.get('/api/bugtracker/v1/projects', token=ctx['werkbank_token'])
|
||||||
|
erwarte_erfolg(r)
|
||||||
|
projekte = r.feld('projects', default=[]) or []
|
||||||
|
erwarte(projekte, 'Leere Projektliste: %s' % r.kurz(300))
|
||||||
|
|
||||||
|
eigen = [p for p in projekte if p.get('slug') == PROJEKT]
|
||||||
|
erwarte(eigen, 'Das eigene Projekt %r fehlt in der Liste.' % PROJEKT)
|
||||||
|
erwarte((eigen[0].get('relation') or {}).get('kind') == 'self',
|
||||||
|
'Das eigene Projekt sollte relation.kind="self" tragen, hat aber %r'
|
||||||
|
% eigen[0].get('relation'))
|
||||||
|
|
||||||
|
|
||||||
|
@test('roadmap', 'Werkbank-Token sieht weiterhin die ganze Landschaft')
|
||||||
|
def _(ctx):
|
||||||
|
"""
|
||||||
|
Die Gegenprobe zum Fall darueber. Der Rueckfall aufs Heimatprojekt darf
|
||||||
|
nur dort greifen, wo ein konkretes Projekt gebraucht wird - beim Lesen
|
||||||
|
ueber alle Projekte hinweg waere er eine Einschraenkung, die es vorher
|
||||||
|
nicht gab.
|
||||||
|
"""
|
||||||
|
r = ctx.client.get('/api/bugtracker/v1/roadmap.php',
|
||||||
|
query={'action': 'list', 'project_slug': 'all'},
|
||||||
|
token=ctx['werkbank_token'])
|
||||||
|
erwarte_erfolg(r)
|
||||||
|
|
||||||
|
|
||||||
@test('roadmap', 'Roadmap-Schreibzugriff verlangt Verwaltungsrecht')
|
@test('roadmap', 'Roadmap-Schreibzugriff verlangt Verwaltungsrecht')
|
||||||
def _(ctx):
|
def _(ctx):
|
||||||
r = ctx.client.post('/api/bugtracker/v1/roadmap.php',
|
r = ctx.client.post('/api/bugtracker/v1/roadmap.php',
|
||||||
|
|||||||
@@ -194,6 +194,47 @@ switch ($action) {
|
|||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Werkbank-Token (owner_type = dev_agent), an ein Projekt gebunden.
|
||||||
|
//
|
||||||
|
// Es gibt keine API, so eines anzulegen - in der Produktion geschieht das
|
||||||
|
// von Hand in der Token-Verwaltung, und das ist auch richtig so. Fuer den
|
||||||
|
// Testlauf fehlte damit aber ausgerechnet der Tokentyp, den der
|
||||||
|
// MCP-Adapter und dc-testkit benutzen. Die Folge war ein Fehler, den
|
||||||
|
// niemand fand: Beim Lesen laesst projectFilter() ein Werkbank-Token
|
||||||
|
// bewusst durch, also war der Projektbezug leer - und
|
||||||
|
// roadmap.php?action=render lehnte mit "missing_project" ab, obwohl das
|
||||||
|
// Token sehr wohl an ein Projekt gebunden ist.
|
||||||
|
//
|
||||||
|
// Ein Tokentyp, den kein Testfall benutzt, ist ein Tokentyp, dessen
|
||||||
|
// Sonderwege niemand prueft.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
case 'workbench_token': {
|
||||||
|
$slug = (string)(Http::str('project_slug') ?? '');
|
||||||
|
if ($slug === '') {
|
||||||
|
Http::fail(400, 'missing_project', 'project_slug wird gebraucht.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$manager = new TokenManager($db);
|
||||||
|
$token = $manager->createMasterToken(
|
||||||
|
(string)(Http::str('name') ?? 'Werkbank (Testlauf)'),
|
||||||
|
$slug,
|
||||||
|
null,
|
||||||
|
'dev_agent',
|
||||||
|
'testkit',
|
||||||
|
['bugtracker:manage', 'supervisor:manage', 'watchdog:read', 'license:dev'],
|
||||||
|
'all'
|
||||||
|
);
|
||||||
|
|
||||||
|
Http::ok([
|
||||||
|
'token_id' => $token['token_id'],
|
||||||
|
'raw_token' => $token['raw_token'],
|
||||||
|
'project_slug' => $slug,
|
||||||
|
'owner_type' => 'dev_agent',
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Signierschluessel fuer den Updateservice. Er wird auf der Testinstanz
|
// Signierschluessel fuer den Updateservice. Er wird auf der Testinstanz
|
||||||
// erzeugt und nie von der Produktion kopiert - ein Testlauf darf mit dem
|
// erzeugt und nie von der Produktion kopiert - ein Testlauf darf mit dem
|
||||||
@@ -260,6 +301,7 @@ switch ($action) {
|
|||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
default:
|
default:
|
||||||
Http::fail(400, 'unknown_action', sprintf('Unbekannte Aktion "%s".', $action), null, [
|
Http::fail(400, 'unknown_action', sprintf('Unbekannte Aktion "%s".', $action), null, [
|
||||||
'actions' => ['status', 'wipe', 'migrate', 'reset', 'genkey', 'log', 'clearlog'],
|
'actions' => ['status', 'wipe', 'migrate', 'reset', 'workbench_token',
|
||||||
|
'genkey', 'log', 'clearlog'],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user