Migrate all repos into monorepo context folders

Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
      Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
      Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)

Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
This commit is contained in:
2026-06-30 20:39:52 +02:00
parent 2f2b295531
commit a5f8fb49ab
1717 changed files with 447332 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
# BEAM MCP Server - Konfiguration
# Kopiere diese Datei nach .env und passe die Werte an (optional)
# ── BEAM URLs (optional, Standardwerte sind vorbelegt) ───────────────────────
# Basis-URL der BEAM Instanz
BEAM_BASE_URL=https://db.leanix.net
# Login-URL (für automatischen Browser-Login)
BEAM_LOGIN_URL=http://db.de/beam
# Workspace-Name (für Token-Key im localStorage)
BEAM_WORKSPACE=beam
# ── Browser-Login (optional) ─────────────────────────────────────────────────
# Headless-Modus für automatischen Browser-Login (true/false)
# Bei Anmeldeproblemen auf false setzen, um den Browser sichtbar zu starten
BEAM_HEADLESS=true
# ── Authentifizierung ────────────────────────────────────────────────────────
# Das BEAM-Token wird automatisch im OS-Keychain gespeichert:
# - macOS: Keychain Access (via `security` CLI)
# - Windows: DPAPI-verschlüsselte Datei in %LOCALAPPDATA%/beam-mcp/
#
# Erstmaliger Login: `npm run login` oder automatisch beim ersten API-Aufruf.
# Danach wird das Token aus dem Keychain geladen — kein erneuter Login nötig.
+6
View File
@@ -0,0 +1,6 @@
.DS_Store
.env
package-lock.json
node_modules
dist
.idea
@@ -0,0 +1,137 @@
---
inclusion: manual
---
# Neues BEAM Tool hinzufügen
Schritt-für-Schritt Anleitung zum Erweitern des beam-mcp-servers um ein neues Tool.
## 1. API-Funktion in `src/api.ts`
Neue Datenabfragen gehören in `api.ts`. Nutze die bestehende `gql()`-Funktion für GraphQL-Queries:
```typescript
export async function myNewFunction(param: string) {
// UUID-Erkennung: wenn keine UUID, per Suche auflösen
let uuid = param;
if (!param.match(/^[0-9a-f-]{36}$/i)) {
const results = await searchFactSheets(param);
if (!results.length) throw new Error(`No factsheet found for: ${param}`);
uuid = results[0].id as string;
}
const data = (await gql(
`query MyQuery($id: ID!) {
factSheet(id: $id) {
id name type
... on Application {
// gewünschte Felder
}
}
}`,
{ id: uuid }
)) as { factSheet: Record<string, unknown> };
return data.factSheet;
}
```
Für REST-Endpunkte (z.B. Bookmarks API) nutze `fetch` mit `getToken()`:
```typescript
const token = await getToken();
const res = await fetch(`${BEAM_BASE_URL}/services/pathfinder/v1/endpoint/${id}`, {
headers: { Authorization: `Bearer ${token}` },
});
```
## 2. Tool registrieren in `src/index.ts`
Importiere die neue Funktion und registriere das Tool:
```typescript
import { myNewFunction } from "./api.js";
server.registerTool(
"beam_my_new_tool", // beam_ Prefix + snake_case
{
title: "Kurzer deutscher Titel",
description: `Ausführliche Beschreibung was das Tool tut.
Args:
- param1 (string, required): Beschreibung
- param2 (string, optional): Beschreibung
Returns:
Was zurückgegeben wird.
Examples:
- param1="Beispielwert" → Was passiert`,
inputSchema: {
param1: z.string()
.min(1, "Darf nicht leer sein")
.describe("Beschreibung des Parameters"),
param2: z.string()
.optional()
.describe("Optionaler Parameter"),
},
annotations: {
readOnlyHint: true, // true wenn nur lesend
destructiveHint: false, // true wenn Daten gelöscht werden
idempotentHint: true, // true wenn wiederholbar ohne Seiteneffekte
openWorldHint: true, // true wenn externe Systeme angesprochen werden
},
},
async ({ param1, param2 }) => {
try {
const result = await myNewFunction(param1);
// Ergebnis formatieren...
const text = `## Ergebnis\n${JSON.stringify(result, null, 2)}`;
return { content: [{ type: "text", text: truncateIfNeeded(text) }] };
} catch (error) {
return handleError(error);
}
}
);
```
## 3. Label-Mappings erweitern (falls nötig)
Wenn neue BEAM-Werte gemappt werden müssen, die bestehenden Maps in `index.ts` erweitern:
```typescript
const LC: Record<string, string> = {
// ... bestehende Einträge
newValue: "Neuer Wert",
};
```
## 4. Build & Test
```bash
npm run build
```
Muss fehlerfrei durchlaufen. Danach MCP-Server neu starten.
## 5. README aktualisieren
Neues Tool in `README.md` unter "Verfügbare Tools" dokumentieren:
- Tool-Name mit `beam_` Prefix
- Parameter-Tabelle
- Annotations
- Beispiele
- Beispiel-Ausgabe
## Checkliste
- [ ] API-Funktion in `api.ts` (nicht in `index.ts`)
- [ ] `server.registerTool()` mit title, description, inputSchema, annotations
- [ ] Tool-Name: `beam_` + snake_case
- [ ] Description mit Args, Returns, Examples
- [ ] Zod-Schema mit `.describe()` und Validierung
- [ ] Alle 4 Annotations gesetzt
- [ ] try/catch mit `handleError()`
- [ ] `truncateIfNeeded()` für große Responses
- [ ] `npm run build` erfolgreich
- [ ] README.md aktualisiert
@@ -0,0 +1,76 @@
---
inclusion: always
---
# BEAM Domänenwissen
## Was ist BEAM?
BEAM (Business Enterprise Architecture Management) ist die Architektur-Plattform der Deutschen Bahn, basierend auf LeanIX. Sie dokumentiert die IT-Landschaft mit Anwendungen, Domänen, IT-Komponenten, Schnittstellen und Diagrammen.
URL: https://db.leanix.net (intern: http://db.de/beam)
## Zentrale Konzepte
### Fact Sheets
Fact Sheets sind die Kernobjekte in BEAM. Jedes hat eine UUID und optional eine externe Beam-ID.
| Typ | Beam-ID Format | Beispiel | Beschreibung |
|-----|---------------|----------|--------------|
| Application | A-XXXXXX | A-106614 | Anwendung/Software-System |
| BusinessCapability | P.X.XX.XX.XX | P.F.05.05.04 | Fachliche Domäne/Capability |
| ITComponent | — | — | Technische Infrastruktur-Komponente |
| Interface | — | — | Schnittstelle zwischen Anwendungen |
| DataObject | — | — | Datenobjekt |
### Lebenszyklus
Jedes Fact Sheet hat einen Lebenszyklus-Status:
- `plan` → Plan
- `phaseIn` → Einführungsphase
- `active` → Aktiv
- `phaseOut` → Ausgliederungsphase
- `endOfLife` → Lebensende
### Supporttyp (Domänen-Zuordnung)
Anwendungen werden Domänen mit einem Supporttyp zugeordnet:
- `leading` → Führend (Hauptanwendung der Domäne)
- `supports` → Unterstützt (Hilfsanwendung)
### Diagramme
Diagramme in BEAM sind als "Bookmarks" gespeichert und enthalten mxGraph-XML (draw.io-kompatibel). Sie werden über die Documents-Relation eines Fact Sheets verknüpft. Typische Diagrammtypen:
- System Context (C4-Modell)
- Verteilungssicht (PROD/TST/ABN)
- Schnittstellen
## BEAM GraphQL API
Endpunkt: `https://db.leanix.net/services/pathfinder/v1/graphql`
Wichtige Queries:
- `allFactSheets(filter, first)` — Suche mit Volltextfilter
- `factSheet(id)` — Einzelnes Fact Sheet mit Relationen
- Relationen: `relApplicationToBusinessCapability`, `relBusinessCapabilityToApplication`, `relApplicationToITComponent`, `relToSuccessor`
## BEAM Bookmarks API (Diagramme)
Endpunkt: `https://db.leanix.net/services/pathfinder/v1/bookmarks/{id}`
Gibt ein JSON-Objekt zurück mit:
- `data.name` — Diagrammname
- `data.groupKey` — Typ (z.B. "freedraw")
- `data.state.graphXml` — mxGraph-XML (draw.io-kompatibel)
- `data.description` — Beschreibung
- `data.referencedFactSheetIds` — Verknüpfte Fact Sheets
## Terminologie-Mapping
Der MCP-Server mappt LeanIX-interne Begriffe auf BEAM-Terminologie:
- Application → Anwendung
- BusinessCapability → Domäne
- ITComponent → IT-Komponente
- Interface → Integration (Schnittstelle)
- lxState APPROVED → Bestätigt
@@ -0,0 +1,86 @@
---
inclusion: always
---
# Coding Standards — beam-mcp-server
## Projektstruktur
```
src/
├── index.ts # MCP Server, Tool-Registrierung, Label-Mappings, Error-Handling
├── api.ts # BEAM GraphQL/REST API Client, Diagramm-Funktionen
├── auth.ts # Token-Management (Keychain + Playwright Browser-Login)
└── keychain.ts # OS-Keychain Abstraktionsschicht (macOS/Windows)
```
## TypeScript-Konventionen
- ESM (`"type": "module"` in package.json), alle Imports mit `.js`-Suffix
- Strict mode aktiviert, kein `any` — nutze `Record<string, unknown>` oder konkrete Interfaces
- Target: ES2022, Module: Node16
- Build: `npm run build` (tsc), Output in `dist/`
## MCP Tool-Konventionen
- Alle Tools nutzen `server.registerTool()` (NICHT `server.tool()`)
- Tool-Namen: `beam_` Prefix + snake_case (z.B. `beam_search_factsheets`)
- Jedes Tool braucht:
- `title`: Kurzer deutscher Titel
- `description`: Ausführliche Beschreibung mit Args, Returns, Examples
- `inputSchema`: Zod-Schemas mit `.describe()` und Validierung (`.min()`, `.max()`)
- `annotations`: `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`
- Jeder Tool-Handler ist in try/catch gewrappt und nutzt `handleError()` bei Fehlern
- Große Responses werden mit `truncateIfNeeded()` auf CHARACTER_LIMIT (25k) gekürzt
## Error-Handling Pattern
```typescript
async (params) => {
try {
// ... Logik ...
return { content: [{ type: "text", text: result }] };
} catch (error) {
return handleError(error);
}
}
```
`handleError()` gibt `{ isError: true, content: [...] }` zurück mit kontextbezogenen Meldungen:
- 401/403 → Hinweis auf `beam_get_token`
- 404 → ID/Suchbegriff prüfen
- 429 → Rate-Limit, warten
## API-Schicht (api.ts)
- `gql()` — Shared GraphQL-Client mit Auth-Header
- Alle API-Funktionen sind `async` und werfen Errors mit HTTP-Status
- UUID-Erkennung: `/^[0-9a-f-]{36}$/i` — alles andere wird als Beam-ID/Name per Suche aufgelöst
- Neue API-Funktionen gehören in `api.ts`, nicht in `index.ts`
## Label-Mappings (index.ts)
BEAM-interne Werte werden auf deutsche Labels gemappt:
- `LC` — Lebenszyklus (active→Aktiv, phaseOut→Ausgliederungsphase, ...)
- `ST` — Supporttyp (leading→Führend, supports→Unterstützt)
- `FS_TYPE` — Fact-Sheet-Typen (Application→Anwendung, ...)
- `STATE` — Status (APPROVED→Bestätigt, ...)
## Auth (auth.ts) & Keychain (keychain.ts)
- Token-Priorität: In-Memory Cache → OS-Keychain → Playwright Browser-Login
- Token wird im OS-Keychain gespeichert (macOS: Keychain Access, Windows: DPAPI)
- Keine .env-Datei für Secrets — nur für Konfigurations-URLs
- Gültigkeit: JWT exp-Claim minus 60s Puffer
- Bei Invalidierung (401): Token aus Memory + Keychain gelöscht
- Logging nur auf stderr (stdio-Transport!)
## Build & Test
```bash
npm run build # TypeScript kompilieren
npm start # Server starten (stdio)
npm run login # Manueller Browser-Login + Token im Keychain speichern
```
Nach jeder Änderung: `npm run build` muss fehlerfrei durchlaufen.
+2
View File
@@ -0,0 +1,2 @@
^[everything]
* @FlorianHofmann
+306
View File
@@ -0,0 +1,306 @@
DB Inner Source Lizenz Version 1.0
_Fachautoren: Cornelius Schumacher, Schlomo Schapiro (DB Systel GmbH)_
Diese Inner-Source-Lizenz für die Deutsche Bahn („DBISL“) gilt für Werke
(im Sinne der nachfolgenden Begriffsbestimmung), die unter
DBISL-Bedingungen zur Verfügung gestellt werden. Das Werk darf nur in
der durch diese Lizenz gestatteten Form genutzt werden (insoweit eine
solche Nutzung dem Urheber vorbehalten ist).
Das Werk wird unter den Bedingungen dieser Lizenz zur Verfügung
gestellt, wenn der Lizenzgeber (im Sinne der nachfolgenden
Begriffsbestimmung) den folgenden Hinweis unmittelbar hinter dem
Urheberrechtshinweis dieses Werks anbringt:
„Lizenziert unter der DBISL“ oder alternativ „Licensed under the DBISL“
oder in einer anderen Form zum Ausdruck bringt, dass er es unter der
DBISL lizenzieren möchte.
== 1. Begriffsbestimmungen
Für diese Lizenz gelten folgende Begriffsbestimmungen:
* „Lizenz“: diese Lizenz.
* „Originalwerk“: das Werk oder die Software, die vom Lizenzgeber unter
dieser Lizenz verbreitet oder zugänglich gemacht wird, und zwar als
Quellcode und gegebenenfalls auch als ausführbarer Code.
* „Bearbeitungen“: die Werke oder Software, die der Lizenznehmer auf der
Grundlage des Originalwerks oder seiner Bearbeitungen schaffen kann. In
dieser Lizenz wird nicht festgelegt, wie umfangreich die Änderung oder
wie stark die Abhängigkeit vom Originalwerk für eine Einstufung als
Bearbeitung sein muss; dies bestimmt sich nach dem anwendbaren
Urheberrecht
* „Werk“: das Originalwerk oder seine Bearbeitungen.
* „Quellcode“: diejenige Form des Werkes, die zur Auffassung durch den
Menschen bestimmt ist und die am besten geeignet ist, um vom Menschen
verstanden und verändert zu werden.
* „Ausführbarer Code“: die — üblicherweise — kompilierte Form des Werks,
die von einem Computer als Programm ausgeführt werden soll.
* „Lizenzgeber“: die juristische Person innerhalb des DB Konzerns, die
das Werk unter der Lizenz verbreitet oder zugänglich macht.
* „Urheberrechtsinhaber/Autor“: jeder, der bestimmte von ihm selbst
entwickelte oder von Dritten vorgegebene Aufgabenstellungen in ein
Originalwerk umsetzt oder am Originalwerk eine Bearbeitung vornimmt.
* „Bearbeiter“: jeder, der das Werk unter der Lizenz verändert oder auf
andere Weise zur Schaffung einer Bearbeitung beiträgt. Jeder Autor ist
auch Bearbeiter.
* „Lizenznehmer“ („Sie“, „Ihnen“): jede juristische Person innerhalb des
DB Konzerns, die das Werk unter den Lizenzbedingungen nutzt.
* „Verbreitung“ oder „Zugänglichmachung“: alle Formen von Verkauf,
Überlassung, Verleih, Vermietung, Verbreitung, Weitergabe, Übermittlung
oder anderweitiger Online- oder Offline-Bereitstellung von
Vervielfältigungen des Werks oder Zugänglichmachung seiner wesentlichen
Funktionen für dritte natürliche oder juristische Personen.
* „Beitrag“: jedes urheberrechtliche Werk, einschließlich des
Originalwerks sowie jeglicher Änderungen, die der Bearbeiter vornimmt,
und die dem Lizenzgeber bewusst zur Aufnahme in das Werk eingereicht
werden.
* „DB“ oder „DB Konzern“: die Deutsche Bahn AG und alle mit ihr nach §
15 AktG verbundenen Unternehmen.
== 2. Umfang der Lizenzrechte
Der Lizenzgeber erteilt Ihnen hiermit eine weltweite, unentgeltliche,
nicht ausschließliche, unterlizenzierbare Lizenz, die Sie für
Geschäftszwecke des DB Konzerns berechtigt:
* das Werk uneingeschränkt zu nutzen,
* das Werk zu vervielfältigen,
* das Werk zu verändern und Bearbeitungen auf der Grundlage des Werks zu
schaffen,
* das Werk oder Vervielfältigungen davon innerhalb der DB zu verbreiten,
Für die Wahrnehmung dieser Rechte können beliebige, derzeit bekannte
oder künftige Medien, Träger und Formate verwendet werden, soweit das
geltende Recht dem nicht entgegensteht.
Der Lizenzgeber erteilt dem Lizenznehmer ein nicht ausschließliches,
unentgeltliches Nutzungsrecht an seinen Patenten, sofern dies zur
Ausübung der durch die Lizenz erteilten Nutzungsrechte am Werk notwendig
ist.
== 3. Zugänglichmachung des Quellcodes
Der Lizenzgeber kann das Werk entweder als Quellcode oder als
ausführbaren Code zur Verfügung stellen. Stellt er es als ausführbaren
Code zur Verfügung, so stellt er darüber hinaus eine maschinenlesbare
Kopie des Quellcodes für jedes von ihm verbreitete
Vervielfältigungsstück des Werks zur Verfügung, oder er verweist in
einem Vermerk im Anschluss an den dem Werk beigefügten
Urheberrechtshinweis auf einen Speicherort, an dem problemlos und
unentgeltlich auf den Quellcode zugegriffen werden kann, solange der
Lizenzgeber das Werk verbreitet oder zugänglich macht.
== 4. Einschränkungen des Urheberrechts
Es ist nicht Zweck dieser Lizenz, Ausnahmen oder Schranken der
ausschließlichen Rechte des Urhebers am Werk, die dem Lizenznehmer
zugutekommen, einzuschränken. Auch die Erschöpfung dieser Rechte bleibt
von dieser Lizenz unberührt.
== 5. Pflichten des Lizenznehmers
Die Einräumung der oben genannten Rechte ist an mehrere Beschränkungen
und Pflichten für den Lizenznehmer gebunden:
* Inner Source: Der Lizenznehmer darf das Werk ausschließlich für
Geschäftszwecke des DB Konzerns nutzen.
* Urheberrechtshinweis, Lizenztext, Nennung des Bearbeiters: Der
Lizenznehmer muss alle Urheberrechts-, Patent- oder Markenrechtshinweise
und alle Hinweise auf die Lizenz und den Haftungsausschluss unverändert
lassen. Jedem von ihm verbreiteten oder zugänglich gemachten
Vervielfältigungsstück des Werks muss der Lizenznehmer diese Hinweise
sowie diese Lizenz beifügen. Der Lizenznehmer muss auf jedem
abgeleiteten Werk deutlich darauf hinweisen, dass das Werk geändert
wurde, und das Datum der Bearbeitung angeben.
* „Copyleft“-Klausel: Der Lizenznehmer darf Vervielfältigungen des
Originalwerks oder Bearbeitungen nur unter den Bedingungen dieser DBISL
oder einer neueren Version dieser Lizenz innerhalb der DB verbreiten
oder zugänglich machen. Der Lizenznehmer (der zum Lizenzgeber wird) darf
für das Werk oder die Bearbeitung keine zusätzlichen Bedingungen
anbieten oder vorschreiben, die die Bedingungen dieser Lizenz verändern
oder einschränken.
* Bereitstellung des Quellcodes: Wenn der Lizenznehmer
Vervielfältigungsstücke des Werks verbreitet oder zugänglich macht, muss
er eine maschinenlesbare Fassung des Quellcodes mitliefern oder einen
Speicherort angeben, über den problemlos und unentgeltlich so lange auf
diesen Quellcode zugegriffen werden kann, wie der Lizenznehmer das Werk
verbreitet oder zugänglich macht.
* Rechtsschutz: Diese Lizenz erlaubt nicht die Benutzung von
Kennzeichen, Marken oder geschützten Namensrechten des Lizenzgebers,
soweit dies nicht für die angemessene und übliche Beschreibung der
Herkunft des Werks und der inhaltlichen Wiedergabe des
Urheberrechtshinweises erforderlich ist.
== 6. Urheber und Bearbeiter
Der ursprüngliche Lizenzgeber gewährleistet, dass er das Urheberrecht am
Originalwerk innehat oder dieses an ihn lizenziert wurde und dass er
befugt ist, diese Lizenz zu erteilen.
Jeder Bearbeiter gewährleistet, dass er das Urheberrecht an den von ihm
vorgenommenen Änderungen des Werks besitzt und befugt ist, einen Beitrag
unter dieser Lizenz zu erstellen und beizutragen.
Für jeden Fall, in dem der Lizenznehmer die Lizenz annimmt, erteilt der
ursprüngliche Lizenzgeber und alle folgenden Bearbeiter eine Befugnis
zur Nutzung der Beiträge zum Werk unter den Bedingungen dieser Lizenz.
== 7. Gewährleistungsausschluss
Falls die konzerninternen Leistungsbedingungen keine Anwendung finden,
gelten die folgenden Regelungen.
Die Arbeit an diesem Werk wird laufend fortgeführt; es wird durch
unzählige Bearbeiter ständig verbessert. Das Werk ist nicht vollendet
und kann daher Fehler („Bugs“) enthalten, die dieser Art der Entwicklung
inhärent sind.
Aus den genannten Gründen wird das Werk unter dieser Lizenz „so, wie es
ist“ ohne jegliche Gewährleistung zur Verfügung gestellt. Dies gilt
unter anderem — aber nicht ausschließlich — für Marktreife,
Verwendbarkeit für einen bestimmten Zweck, Mängelfreiheit, Richtigkeit
sowie Nichtverletzung von anderen Immaterialgüterrechten als dem
Urheberrecht (vgl. dazu Artikel 6 dieser Lizenz).
Dieser Gewährleistungsausschluss ist wesentlicher Bestandteil der Lizenz
und Bedingung für die Einräumung von Rechten an dem Werk.
== 8. Haftungsausschluss/Haftungsbeschränkung
Falls die konzerninternen Leistungsbedingungen keine Anwendung finden,
gelten die folgenden Regelungen.
Außer in Fällen von Vorsatz oder der Verursachung von Personenschäden
haftet der Lizenzgeber nicht für direkte oder indirekte, materielle oder
immaterielle Schäden irgendwelcher Art, die aus der Lizenz oder der
Benutzung des Werks folgen; dies gilt unter anderem, aber nicht
ausschließlich, für Firmenwertverluste, Produktionsausfall,
Computerausfall oder Computerfehler, Datenverlust oder wirtschaftliche
Schäden, und zwar auch dann, wenn der Lizenzgeber auf die Möglichkeit
solcher Schäden hingewiesen wurde. Unabhängig davon haftet der
Lizenzgeber im Rahmen der gesetzlichen Produkthaftung, soweit die
entsprechenden Regelungen auf das Werk anwendbar sind.
== 9. Zusatzvereinbarungen
Wenn der Lizenznehmer das Werk verbreitet, kann er Zusatzvereinbarungen
schließen, in denen Verpflichtungen oder Dienstleistungen festgelegt
werden, die mit dieser Lizenz vereinbar sind.
Der Lizenznehmer darf Verpflichtungen nur in seinem eigenen Namen
eingehen, nicht jedoch im Namen des ursprünglichen Lizenzgebers oder
eines anderen Bearbeiters, und nur, wenn er sich gegenüber allen
Bearbeitern verpflichtet, sie zu entschädigen, zu verteidigen und von
der Haftung freizustellen, falls aufgrund der von ihm eingegangenen
Gewährleistungsverpflichtung oder Haftungsübernahme Forderungen gegen
sie geltend gemacht werden oder eine Haftungsverpflichtung entsteht.
== 10. Annahme der Lizenz
Der Lizenznehmer stimmt den Bestimmungen dieser Lizenz zu, indem er das
Symbol „Lizenz annehmen“ unter dem Fenster mit dem Lizenztext anklickt
oder indem er seine Zustimmung auf vergleichbare Weise gibt. Das
Anklicken des Symbols gilt als Anzeichen der eindeutigen und
unwiderruflichen Annahme der Lizenz und der darin enthaltenen Klauseln
und Bedingungen.
In gleicher Weise gilt als Zeichen der eindeutigen und unwiderruflichen
Zustimmung die Ausübung eines Rechtes, das in Artikel 2 dieser Lizenz
angeführt ist, wie das Erstellen einer Bearbeitung oder die Verbreitung
oder Zugänglichmachung des Werks oder dessen Vervielfältigungen.
== 11. Informationspflichten
Wenn der Lizenznehmer das Werk verbreitet oder zugänglich macht
(beispielsweise, indem er es zum Herunterladen von einer Website
anbietet), muss der Lizenznehmer über den Vertriebskanal oder das
benutzte Verbreitungsmedium dem Adressatenkreis bzw. der Öffentlichkeit
Mindest-Informationen bereitstellen, üblicherweise bezüglich der
Lizenzgeber, der Lizenz und ihrer Zugänglichkeit, des Abschlusses des
Lizenzvertrags sowie darüber, wie die Lizenz durch den Lizenznehmer
gespeichert und vervielfältigt werden kann.
== 12. Beendigung der Lizenz
Die Lizenz und die damit eingeräumten Rechte erlöschen automatisch, wenn
der Lizenznehmer gegen die Lizenzbedingungen verstößt.
Ein solches Erlöschen der Lizenz führt nicht zum Erlöschen der Lizenzen
von Dritten, denen das Werk vom Lizenznehmer unter dieser Lizenz zur
Verfügung gestellt worden ist, solange diese Personen die
Lizenzbedingungen erfüllen.
== 13. Einreichung von Beiträgen
Sofern nichts ausdrücklich anderes angegeben, unterliegt jeder Beitrag,
den der Lizenzgeber bewusst zur Aufnahme in das Werk eingereicht hat,
den Bedingungen dieser Lizenz, ohne dass zusätzliche Bedingungen gelten.
Ungeachtet des Vorstehenden ersetzt oder ändert keine der hierin
enthaltenen Bestimmungen die Bedingungen einer separaten
Lizenzvereinbarung, die der Auftraggeber möglicherweise mit dem
Auftragnehmer für solche Beiträge abgeschlossen hat.
Für die Länder, in denen Urheberpersönlichkeitsrechte an einem Werk
entstehen können, verzichtet der Urheberrechtsinhaber/Autor im
gesetzlich zulässigen Umfang auf seine Urheberpersönlichkeitsrechte, um
die Lizenzierung der oben aufgeführten Verwertungsrechte wirksam
durchführen zu können.
== 14. Sonstiges
Unbeschadet des Artikels 9 stellt diese Lizenz die vollständige
Vereinbarung der Parteien über das Werk dar.
Es gilt deutsches Recht. Sind einzelne Bestimmungen der Lizenz nach
geltendem Recht nichtig oder unwirksam, so berührt dies nicht die
Wirksamkeit oder Durchsetzbarkeit der Lizenz an sich. Solche
Bestimmungen werden vielmehr dergestalt ausgelegt oder modifiziert, dass
sie wirksam und durchsetzbar sind.
== 15. Gesellschaftsrechtliche Veränderungen
Bei gesellschaftsrechtlichen Veränderungen, z.B. dem Verkauf oder der
Abspaltung einer DB Gesellschaft, gilt folgende Regelungen in Anlehnung
an §12 Beendigungsunterstützung der konzerninternen
Leistungsbedingungen:
Eine weitere Nutzung der lizenzierten Software durch ein nicht mehr dem
DB Konzern angehöriges Unternehmen unterliegt der Zustimmung durch die
Urheber bzw. das CIO Board.
== 16. Lizenzänderungen
Die Urheber eines Werks können gemeinsam eine Änderung der Lizenz
entscheiden, z.B. um das Werk als Open Source Software zu
veröffentlichen. Falls die Urheber nicht verfügbar sind oder sich nicht
einigen können, so kann das CIO Board stellvertretend für alle Urheber
innerhalb der DB die Änderung der Lizenz für ein Werk beschließen.
== 17. Streitbeilegung
Unbeschadet der Regelungen in den konzerninternen Leistungsbedingungen
zwischen den Parteien gilt zwischen den Parteien Folgendes:
Bei Streitigkeiten im Zusammenhang mit der Auslegung und Anwendung
dieser Lizenz, bei denen mehr als ein Konzernunternehmen beteiligt ist,
dient das CIO Board des Konzerns als Entscheidungsgremium, welches von
jeder Partei angerufen werden kann.
== 18. Lizenz der Lizenz
Dieser Lizenztext ist lizenziert unter einer
„https://creativecommons.org/licenses/by/4.0/[Creative Commons
Namensnennung 4.0 International Lizenz]“ (CC-BY 4.0).
Sie dürfen diesen Lizenztext für sich kopieren und anpassen, solange Sie
dabei die Deutsche Bahn Marke und „DB“ nur innerhalb der DB benutzen.
Falls Sie das Material für die Verwendung außerhalb der DB anpassen, so
müssen Sie alle Nennungen der Deutschen Bahn und DB ersetzen bzw.
entfernen. Geänderte Versionen des Lizenztextes müssen klar als
geänderte Versionen kenntlich gemacht werden.
Teile des Textes dieser Lizenz basieren auf der EU Public License (EUPL)
v1.2.
+369
View File
@@ -0,0 +1,369 @@
*DIESER MCP SERVER DIENT NUR FÜR "EDUCATIONAL PURPOSES"! EINE AKTIVE VERWENDUNG IST NICHT GESTATTET AUFGRUND DATENSCHUTZRECHTLICHER BEDENKEN!*
# BEAM MCP Server
MCP Server für die Deutsche Bahn Enterprise Architektur Plattform **BEAM** (http://db.de/beam).
Ermöglicht den direkten Zugriff auf BEAM-Daten über die LeanIX GraphQL API — ohne Browser-Navigation.
## Ansprechpartner
| Kontakt | E-Mail |
|---------|--------|
| Team DT-BETA | DBS.BS.INFRA.DS4E.DT-BETA@deutschebahn.com |
## Lizenz
DB Inner Source License (DBISL) - DB Systel GmbH
---
## Voraussetzungen
- Node.js >= 18
- Microsoft Edge (für automatischen Browser-Login)
- Zugang zu BEAM (DB-Netz oder VPN)
---
## Installation
```bash
cd beam-mcp
npm install
npm run build
```
---
## Konfiguration
Optional: Kopiere `.env.example` nach `.env` um die Standard-URLs anzupassen:
```bash
cp .env.example .env
```
### Umgebungsvariablen
| Variable | Beschreibung | Standard |
|---|---|---|
| `BEAM_BASE_URL` | Basis-URL der BEAM-Instanz | `https://db.leanix.net` |
| `BEAM_LOGIN_URL` | Login-URL für automatischen Browser-Login | `http://db.de/beam` |
| `BEAM_WORKSPACE` | Workspace-Name | `beam` |
| `BEAM_HEADLESS` | Browser-Login im Headless-Modus (`true`/`false`). Bei Anmeldeproblemen auf `false` setzen, um den Browser sichtbar zu starten. | `true` |
---
## Authentifizierung
Das BEAM-Token wird sicher im **OS-Keychain** gespeichert:
- **macOS**: Keychain Access (via `security` CLI)
- **Windows**: DPAPI-verschlüsselte Datei in `%LOCALAPPDATA%/beam-mcp/`
### Automatischer Browser-Login (empfohlen)
Beim ersten Aufruf eines Tools ohne gültiges Token startet der Server automatisch einen Microsoft Edge Browser, navigiert zu BEAM und meldet sich über "BEAM: DB User" an. Das Token wird anschließend im OS-Keychain gespeichert und für alle weiteren Anfragen (auch nach Server-Neustart) wiederverwendet.
Falls der automatische Login fehlschlägt, kann der Browser mit `BEAM_HEADLESS=false` sichtbar gestartet werden, um das Problem zu diagnostizieren.
Der Login kann auch manuell ausgelöst werden:
```bash
npm run login
```
Oder über das Tool `beam_get_token` im MCP-Client.
### Token-Lebensdauer
BEAM-Tokens sind zeitlich begrenzt gültig. Der Server prüft die Gültigkeit automatisch und löst bei Bedarf einen neuen Login aus. Bei einem 401-Fehler wird das gespeicherte Token aus dem Keychain gelöscht und ein neuer Login getriggert.
---
## Integration
### Kiro
In `.kiro/settings/mcp.json`:
```json
{
"mcpServers": {
"beam-mcp": {
"command": "node",
"args": ["/pfad/zu/beam-mcp/dist/index.js"],
"env": {
"BEAM_BASE_URL": "https://db.leanix.net",
"BEAM_WORKSPACE": "beam",
"BEAM_HEADLESS": "true"
}
}
}
}
```
### OpenCode
In `.opencode/opencode.json`:
```json
{
"mcp": {
"beam-mcp": {
"enabled": true,
"type": "local",
"command": ["node", "/pfad/zu/beam-mcp/dist/index.js"],
"environment": {
"BEAM_BASE_URL": "https://db.leanix.net",
"BEAM_WORKSPACE": "beam",
"BEAM_HEADLESS": "true"
}
}
}
}
```
---
## Verfügbare Tools
Alle Tools verwenden das Prefix `beam_` um Namenskonflikte mit anderen MCP-Servern zu vermeiden.
### `beam_get_token`
Meldet sich bei BEAM an und speichert das Token im OS-Keychain.
Nur nötig, wenn kein gültiges Token vorhanden ist oder das Token abgelaufen ist.
**Parameter:** keine
**Annotations:** `readOnlyHint: false` | `idempotentHint: true`
**Beispiel:**
```
beam_get_token()
```
**Ausgabe:**
```
✓ BEAM Login erfolgreich!
Benutzer: florian.hofmann@deutschebahn.com
Token gültig bis: 20.04.2026, 11:30:00
Token im Keychain gespeichert.
```
---
### `beam_search_factsheets`
Sucht nach BEAM Fact Sheets anhand von Name, Beam-ID oder Freitext.
**Parameter:**
| Parameter | Typ | Pflicht | Beschreibung |
|---|---|---|---|
| `query` | String | ✓ | Suchbegriff, Beam-ID (z.B. `A-106614`) oder Name |
| `type` | Enum | — | Einschränkung auf Fact-Sheet-Typ (siehe unten) |
**Mögliche Typen:**
- `Application` — Anwendung
- `BusinessCapability` — Domäne
- `ITComponent` — IT-Komponente
- `Interface` — Integration
- `DataObject` — Datenobjekt
**Annotations:** `readOnlyHint: true` | `idempotentHint: true`
**Beispiele:**
```
beam_search_factsheets(query: "A-106614")
beam_search_factsheets(query: "RiM", type: "Application")
beam_search_factsheets(query: "Personaleinsatz", type: "BusinessCapability")
```
---
### `beam_get_factsheet`
Ruft vollständige Details eines BEAM Fact Sheets ab.
**Parameter:**
| Parameter | Typ | Pflicht | Beschreibung |
|---|---|---|---|
| `id` | String | ✓ | Beam-ID (z.B. `A-106614`) oder UUID |
**Unterstützte Fact-Sheet-Typen:**
- Anwendungen (`Application`)
- Domänen (`BusinessCapability`)
- IT-Komponenten (`ITComponent`)
**Annotations:** `readOnlyHint: true` | `idempotentHint: true`
**Beispiele:**
```
beam_get_factsheet(id: "A-106614")
beam_get_factsheet(id: "33cb34f7-d6fe-481e-a650-8ca42884c4eb")
beam_get_factsheet(id: "P.F.05.05.04")
```
**Ausgabe (Anwendung):**
```
## A-106614 - Fahrendes Personal RiM
Typ: Anwendung
Status: Bestätigt
Alias: RiM; Rail in Motion; Rim; rim
Lebenszyklus: Ausgliederungsphase
Beschreibung:
Mit "Fahrendes Personal RiM" wird Fahrpersonalen und Triebfahrzeugführern...
Tags: Bordservice, Mobile Solutions Bordpersonale
### Domänen
• P.F.05.05.04 - Informationen bereitstellen | Supporttyp: Führend
### IT-Komponenten
• Enterprise Cloud Managed
### Nachfolger
• MERKUR (ID: 8c92a72a-...)
```
---
### `beam_get_domain_applications`
Listet alle Anwendungen einer BEAM-Domäne auf, gruppiert nach Supporttyp.
**Parameter:**
| Parameter | Typ | Pflicht | Beschreibung |
|---|---|---|---|
| `domain` | String | ✓ | Domänen-ID (z.B. `P.F.05.05.04`), Name oder UUID |
**Annotations:** `readOnlyHint: true` | `idempotentHint: true`
**Beispiele:**
```
beam_get_domain_applications(domain: "P.F.05.05.04")
beam_get_domain_applications(domain: "Informationen bereitstellen")
```
---
### `beam_get_diagram`
Ruft Diagramme einer BEAM-Anwendung ab. Listet verfügbare Diagramme auf, zeigt eine lesbare Zusammenfassung (Systeme, Schnittstellen) und kann das Diagramm als `.drawio`-Datei speichern.
**Parameter:**
| Parameter | Typ | Pflicht | Beschreibung |
|---|---|---|---|
| `application` | String | ✓ | Anwendungsname, Beam-ID (z.B. `A-109410`) oder UUID |
| `diagram_name` | String | — | Name oder Teil des Diagrammnamens zum Abrufen |
| `save_path` | String | — | Dateipfad zum Speichern als `.drawio`-Datei |
**Annotations:** `readOnlyHint: false` (kann Dateien schreiben) | `idempotentHint: true`
**Beispiele:**
Diagramme auflisten:
```
beam_get_diagram(application: "KEP_KEO_Revision")
```
Diagramm lesen:
```
beam_get_diagram(application: "KEP_KEO_Revision", diagram_name: "System Context")
```
Diagramm als draw.io speichern:
```
beam_get_diagram(application: "KEP_KEO_Revision", diagram_name: "System Context", save_path: "./kep-system-context.drawio")
```
**Ausgabe (Lesen):**
```
## A-109410 - KEP_KEO_Revision - System Context
Typ: freedraw
Beschreibung: Systemkontextdiagramm zu "Anwendung zur Integration..."
### Systeme/Akteure (10)
• A-109410 - KEP_KEO_Revision [Software System] — Kraftwerkseinsatzoptimierung
• A-107939 - ETRM_Marktdatendatenbank [Software System] — Energiemarktrelevante Daten
...
### Schnittstellen/Verbindungen (12)
• Auswahl gewählter Produktionsvolumen für D und D+1
• Marktpreise
...
```
**Ausgabe (Speichern):**
```
✓ Diagramm gespeichert: /pfad/zu/kep-system-context.drawio
Die Datei kann direkt in draw.io geöffnet werden.
```
**Hinweise:**
- Die `.drawio`-Datei enthält das originale mxGraph-XML aus BEAM und kann direkt in draw.io (Desktop oder Web) geöffnet werden.
- Die lesbare Zusammenfassung extrahiert automatisch C4-Modell-Elemente (Systeme, Personen) und Schnittstellen.
---
## Fehlerbehandlung
Alle Tools geben bei Fehlern strukturierte Fehlermeldungen mit `isError: true` zurück:
| Fehler | Meldung | Lösung |
|--------|---------|--------|
| 401/403 | Authentifizierung fehlgeschlagen | `beam_get_token` aufrufen |
| 404 | Nicht gefunden | ID oder Suchbegriff prüfen |
| 429 | Rate-Limit erreicht | Kurz warten, erneut versuchen |
Große Antworten werden automatisch auf 25.000 Zeichen gekürzt mit einem Hinweis auf spezifischere Filter.
---
## Architektur
```
beam-mcp/
├── src/
│ ├── index.ts # MCP Server, Tool-Registrierung (registerTool), Error-Handling
│ ├── api.ts # BEAM GraphQL API Client, Diagramm-Export
│ ├── auth.ts # Authentifizierung (Keychain + Playwright Login)
│ └── keychain.ts # OS-Keychain Abstraktionsschicht (macOS/Windows)
├── dist/ # Kompilierter JavaScript-Code (nach npm run build)
├── .env.example # Vorlage für optionale Konfiguration
├── package.json
└── tsconfig.json
```
### Datenfluss
```
Kiro / OpenCode / KI-Assistent
beam-mcp-server (MCP über stdio)
├─► auth.ts ──► keychain.ts ──► OS-Keychain (Token lesen/schreiben)
│ └─► Playwright Edge Login (falls kein gültiges Token)
└─► api.ts ──► BEAM GraphQL API (https://db.leanix.net/services/pathfinder/v1/graphql)
└─► BEAM Bookmarks API (Diagramme)
```
---
## Hinweise
- Das Token läuft nach einigen Stunden ab. Der Server erkennt dies automatisch und löst einen neuen Login aus.
- Der Browser-Login benötigt Zugang zu `http://db.de/beam` (DB-Netz oder VPN).
- Alle internen Variablen- und Feldnamen folgen der LeanIX API-Konvention; die Anzeige ist auf BEAM-Terminologie gemappt.
- Der Server ist read-only — es werden keine Daten in BEAM verändert (außer `.drawio`-Dateien lokal speichern).
- Alle Tools nutzen `registerTool` mit Annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`).
- Tool-Namen verwenden das `beam_`-Prefix zur Vermeidung von Namenskonflikten.
- Keine Secrets in Dateien — Token wird ausschließlich im OS-Keychain gespeichert.
+21
View File
@@ -0,0 +1,21 @@
{
"name": "beam-mcp",
"version": "1.1.0",
"description": "MCP Server für BEAM (Deutsche Bahn Enterprise Architektur)",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsc --watch",
"login": "npm run build && node dist/login.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.6.1",
"playwright": "^1.59.1"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.0.0"
}
}
+6
View File
@@ -0,0 +1,6 @@
---
version: v3
confidentiality: internal
contacts: florian.hofmann@deutschebahn.com
license: LicenseRef-DBPROPRIETARY
reference-ids: none
+306
View File
@@ -0,0 +1,306 @@
import { getToken, invalidateToken, BEAM_BASE_URL } from "./auth.js";
import { writeFileSync } from "fs";
import { resolve } from "path";
const GRAPHQL_URL = `${BEAM_BASE_URL}/services/pathfinder/v1/graphql`;
async function gql(query: string, variables?: Record<string, unknown>, retry = true): Promise<unknown> {
const token = await getToken();
const res = await fetch(GRAPHQL_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query, variables }),
});
if (res.status === 401 && retry) {
invalidateToken();
return gql(query, variables, false);
}
if (!res.ok) throw new Error(`GraphQL HTTP ${res.status}: ${await res.text()}`);
const json = (await res.json()) as { data?: unknown; errors?: unknown[] };
if (json.errors?.length) throw new Error(`GraphQL errors: ${JSON.stringify(json.errors)}`);
return json.data;
}
// ── Search factsheets ────────────────────────────────────────────────────────
export async function searchFactSheets(query: string, type?: string) {
const filter: Record<string, unknown> = { fullTextSearch: query };
if (type) filter.facetFilters = [{ facetKey: "FactSheetTypes", keys: [type] }];
const data = (await gql(
`query Search($filter: FilterInput) {
allFactSheets(filter: $filter, first: 20) {
edges {
node {
id name type
... on Application {
lxState
externalId { externalId }
alias
lifecycle { asString }
}
... on BusinessCapability {
externalId { externalId }
lifecycle { asString }
}
... on ITComponent {
externalId { externalId }
lifecycle { asString }
}
}
}
}
}`,
{ filter }
)) as { allFactSheets: { edges: { node: Record<string, unknown> }[] } };
return data.allFactSheets.edges.map((e) => e.node);
}
// ── Get full factsheet details ───────────────────────────────────────────────
export async function getFactSheet(idOrBeamId: string) {
let uuid = idOrBeamId;
if (!idOrBeamId.match(/^[0-9a-f-]{36}$/i)) {
const results = await searchFactSheets(idOrBeamId);
if (!results.length) throw new Error(`No factsheet found for: ${idOrBeamId}`);
uuid = results[0].id as string;
}
const data = (await gql(
`query GetFS($id: ID!) {
factSheet(id: $id) {
id name type
... on Application {
lxState
externalId { externalId }
alias
description
lifecycle { asString }
tags { name }
relApplicationToBusinessCapability {
edges {
node {
supportType
functionalSuitability
factSheet {
id name
... on BusinessCapability { externalId { externalId } }
}
}
}
}
relApplicationToITComponent {
edges { node { factSheet { id name } } }
}
relToSuccessor {
edges { node { factSheet { id name } } }
}
}
... on BusinessCapability {
externalId { externalId }
description
lifecycle { asString }
relBusinessCapabilityToApplication {
edges {
node {
supportType
functionalSuitability
factSheet {
id name type
... on Application {
lxState
externalId { externalId }
alias
lifecycle { asString }
}
}
}
}
}
}
... on ITComponent {
externalId { externalId }
description
lifecycle { asString }
}
}
}`,
{ id: uuid }
)) as { factSheet: Record<string, unknown> };
return data.factSheet;
}
// ── Get all applications of a domain ────────────────────────────────────────
export async function getDomainApplications(domainIdOrExternalId: string) {
let uuid = domainIdOrExternalId;
if (!domainIdOrExternalId.match(/^[0-9a-f-]{36}$/i)) {
const results = await searchFactSheets(domainIdOrExternalId, "BusinessCapability");
if (!results.length) throw new Error(`No domain found for: ${domainIdOrExternalId}`);
uuid = results[0].id as string;
}
const data = (await gql(
`query DomainApps($id: ID!) {
factSheet(id: $id) {
id name
... on BusinessCapability {
externalId { externalId }
relBusinessCapabilityToApplication {
edges {
node {
supportType
functionalSuitability
factSheet {
id name type
... on Application {
lxState
externalId { externalId }
alias
lifecycle { asString }
tags { name }
}
}
}
}
}
}
}
}`,
{ id: uuid }
)) as { factSheet: Record<string, unknown> };
return data.factSheet;
}
// ── Get diagram (bookmark) ──────────────────────────────────────────────────
export async function getDiagram(id: string) {
const token = await getToken();
const res = await fetch(
`${BEAM_BASE_URL}/services/pathfinder/v1/bookmarks/${id}`,
{ headers: { Authorization: `Bearer ${token}` } }
);
if (!res.ok) throw new Error(`Bookmark HTTP ${res.status}: ${await res.text()}`);
return (await res.json()) as { data: Record<string, unknown> };
}
// ── Get diagrams linked to a fact sheet via documents ───────────────────────
export async function getFactSheetDiagrams(idOrName: string) {
let uuid = idOrName;
if (!idOrName.match(/^[0-9a-f-]{36}$/i)) {
const results = await searchFactSheets(idOrName);
if (!results.length) throw new Error(`No factsheet found for: ${idOrName}`);
uuid = results[0].id as string;
}
const data = (await gql(
`query Docs($id: ID!) {
factSheet(id: $id) { id name documents { edges { node { id name } } } }
}`,
{ id: uuid }
)) as { factSheet: { id: string; name: string; documents: { edges: { node: { id: string; name: string } }[] } } };
const diagramDocs = data.factSheet.documents.edges
.filter((e) => e.node.name.startsWith("Diagram "))
.map((e) => e.node.name.replace("Diagram ", ""));
const diagrams: { id: string; name: string; groupKey: string }[] = [];
for (const bmId of diagramDocs) {
try {
const bm = (await getDiagram(bmId)).data;
diagrams.push({ id: bmId, name: bm.name as string, groupKey: bm.groupKey as string });
} catch { /* skip inaccessible */ }
}
return { factSheet: data.factSheet, diagrams };
}
// ── Extract graphXml from diagram bookmark data ─────────────────────────────
export function extractGraphXml(bookmarkData: Record<string, unknown>): string | null {
const state = bookmarkData.state as Record<string, unknown> | undefined;
if (!state?.graphXml) return null;
return state.graphXml as string;
}
// ── Save diagram as .drawio file ────────────────────────────────────────────
export function saveDiagramAsDrawio(
graphXml: string,
diagramName: string,
outputPath: string
): string {
// draw.io expects the mxGraphModel wrapped in a <diagram> inside <mxfile>
const safeName = diagramName.replace(/[<>&"']/g, "");
const drawioXml = `<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="beam-mcp" modified="${new Date().toISOString()}" type="device">
<diagram name="${safeName}">
${graphXml}
</diagram>
</mxfile>`;
const fullPath = resolve(outputPath);
writeFileSync(fullPath, drawioXml, "utf8");
return fullPath;
}
// ── Parse diagram into readable text summary ────────────────────────────────
export function parseDiagramToText(bookmarkData: Record<string, unknown>): string {
const lines: string[] = [];
const name = bookmarkData.name as string ?? "Unbekannt";
const desc = bookmarkData.description as string ?? "";
const groupKey = bookmarkData.groupKey as string ?? "";
const updatedAt = bookmarkData.updatedAt as string ?? "";
lines.push(`## ${name}`);
if (groupKey) lines.push(`Typ: ${groupKey}`);
if (desc) lines.push(`Beschreibung: ${desc}`);
if (updatedAt) lines.push(`Zuletzt aktualisiert: ${updatedAt}`);
// Parse graphXml to extract nodes and edges
const graphXml = extractGraphXml(bookmarkData);
if (!graphXml) {
lines.push("\nKein graphXml im Diagramm gefunden.");
return lines.join("\n");
}
// Extract nodes (factSheet objects with c4Name)
const nodeRegex = /c4Name="([^"]*)"[^>]*c4Type="([^"]*)"[^>]*c4Description="([^"]*)"[^>]*factSheetId="([^"]*)"/g;
const nodes: { name: string; type: string; desc: string; id: string }[] = [];
let match: RegExpExecArray | null;
while ((match = nodeRegex.exec(graphXml)) !== null) {
nodes.push({
name: match[1].replace(/&amp;/g, "&").replace(/&#10;/g, " ").replace(/<[^>]*>/g, ""),
type: match[2],
desc: match[3].replace(/&amp;/g, "&").replace(/&#10;/g, " ").replace(/<[^>]*>/g, ""),
id: match[4],
});
}
// Extract edges (interface labels)
const edgeRegex = /label="([^"]*)"[^>]*factSheetType="Interface"[^>]*factSheetId="([^"]*)"/g;
const edges: { label: string; id: string }[] = [];
while ((match = edgeRegex.exec(graphXml)) !== null) {
edges.push({
label: match[1].replace(/&amp;/g, "&").replace(/&#10;/g, " "),
id: match[2],
});
}
if (nodes.length) {
lines.push(`\n### Systeme/Akteure (${nodes.length})`);
nodes.forEach((n) => {
lines.push(`${n.name} [${n.type}]${n.desc ? " — " + n.desc : ""}`);
});
}
if (edges.length) {
lines.push(`\n### Schnittstellen/Verbindungen (${edges.length})`);
edges.forEach((e) => {
lines.push(`${e.label}`);
});
}
return lines.join("\n");
}
+127
View File
@@ -0,0 +1,127 @@
/**
* Auth-Modul: Stellt ein gültiges BEAM JWT-Token bereit.
*
* Priorität:
* 1. In-Memory Cache (schnellster Zugriff)
* 2. OS-Keychain (macOS Keychain / Windows DPAPI)
* 3. Playwright Browser-Login (automatisch wenn kein Token vorhanden)
*
* Token wird nach erfolgreichem Login im OS-Keychain persistiert.
*/
import { existsSync, mkdirSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import { tmpdir } from "os";
import { getStoredToken, storeToken, deleteStoredToken } from "./keychain.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
export const BEAM_BASE_URL = process.env.BEAM_BASE_URL ?? "https://db.leanix.net";
export const BEAM_LOGIN_URL = process.env.BEAM_LOGIN_URL ?? "http://db.de/beam";
const WORKSPACE = process.env.BEAM_WORKSPACE ?? "beam";
const TOKEN_KEY = `lxAccessToken:${WORKSPACE}`;
// Temporäres Profil-Verzeichnis (kein Konflikt mit laufendem Edge)
const TEMP_PROFILE_DIR = join(tmpdir(), "beam-mcp-edge-profile");
// In-Memory Cache
let cachedToken: string | null = null;
// ── Token-Gültigkeit prüfen ───────────────────────────────────────────────────
function parseExpiry(token: string): number {
try {
const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
return (payload.exp ?? 0) * 1000;
} catch {
return 0;
}
}
function isValid(token: string): boolean {
return parseExpiry(token) > Date.now() + 60_000;
}
// ── Playwright Login ──────────────────────────────────────────────────────────
export async function loginWithBrowser(): Promise<string> {
process.stderr.write("[BEAM] Kein gültiges Token gefunden. Starte Edge Login...\n");
const { chromium } = await import("playwright");
if (!existsSync(TEMP_PROFILE_DIR)) mkdirSync(TEMP_PROFILE_DIR, { recursive: true });
process.stderr.write(`[BEAM] Temporäres Profil: ${TEMP_PROFILE_DIR}\n`);
const context = await chromium.launchPersistentContext(TEMP_PROFILE_DIR, {
channel: "msedge",
headless: false,
ignoreDefaultArgs: [
"--disable-background-networking",
"--disable-extensions",
"--disable-sync",
"--no-sandbox",
],
args: ["--no-first-run", "--no-default-browser-check"],
});
try {
const page = await context.newPage();
await page.goto(BEAM_LOGIN_URL, { waitUntil: "domcontentloaded", timeout: 30000 });
// Klicke auf "BEAM: DB User" falls Login-Seite erscheint
const loginLink = page.getByRole("link", { name: /BEAM.*DB User/i })
.or(page.getByText(/BEAM.*DB User/i))
.first();
const isLoginPage = await loginLink.isVisible({ timeout: 5000 }).catch(() => false);
if (isLoginPage) {
await loginLink.click({ timeout: 10000 });
}
// Warte auf erfolgreichen Login (URL enthält leanix.net)
await page.waitForURL(/leanix\.net/, { timeout: 180000 });
// Warte bis Token im localStorage liegt
await page.waitForFunction(
(key) => !!localStorage.getItem(key),
TOKEN_KEY,
{ timeout: 30000 }
);
const token = await page.evaluate((key) => localStorage.getItem(key), TOKEN_KEY);
if (!token) throw new Error("Token nicht im localStorage gefunden nach Login.");
// Token im Keychain persistieren
storeToken(token);
process.stderr.write("[BEAM] Login erfolgreich. Token im Keychain gespeichert.\n");
return token;
} finally {
await context.close();
}
}
// ── Haupt-Funktion: Token bereitstellen ──────────────────────────────────────
export async function getToken(): Promise<string> {
// 1. In-Memory Cache
if (cachedToken && isValid(cachedToken)) return cachedToken;
// 2. OS-Keychain
const keychainToken = getStoredToken();
if (keychainToken && isValid(keychainToken)) {
cachedToken = keychainToken;
process.stderr.write("[BEAM] Token aus Keychain geladen.\n");
return cachedToken;
}
// 3. Playwright Login
const token = await loginWithBrowser();
cachedToken = token;
return token;
}
// ── Token invalidieren (bei 401) ─────────────────────────────────────────────
export function invalidateToken(): void {
cachedToken = null;
deleteStoredToken();
process.stderr.write("[BEAM] Token invalidiert (Memory + Keychain).\n");
}
+480
View File
@@ -0,0 +1,480 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import {
searchFactSheets,
getFactSheet,
getDomainApplications,
getDiagram,
getFactSheetDiagrams,
extractGraphXml,
saveDiagramAsDrawio,
parseDiagramToText,
} from "./api.js";
import { getToken, loginWithBrowser, invalidateToken } from "./auth.js";
const CHARACTER_LIMIT = 25_000;
const server = new McpServer({
name: "beam-mcp-server",
version: "1.1.0",
});
// ── Label-Mappings ────────────────────────────────────────────────────────────
const LC: Record<string, string> = {
active: "Aktiv",
plan: "Plan",
phaseIn: "Einführungsphase",
phaseOut: "Ausgliederungsphase",
endOfLife: "Lebensende",
};
const lcLabel = (s: string | undefined) => (s ? (LC[s] ?? s) : "—");
const ST: Record<string, string> = {
leading: "Führend",
supports: "Unterstützt",
};
const stLabel = (s: string | null | undefined) => (s ? (ST[s] ?? s) : "—");
const FS_TYPE: Record<string, string> = {
Application: "Anwendung",
BusinessCapability: "Domäne",
ITComponent: "IT-Komponente",
Interface: "Integration",
DataObject: "Datenobjekt",
TechnicalStack: "Technologie-Stack",
UserGroup: "Nutzergruppe",
Process: "Prozess",
Epic: "Initiative",
};
const typeLabel = (s: string) => FS_TYPE[s] ?? s;
const STATE: Record<string, string> = {
APPROVED: "Bestätigt",
DRAFT: "Entwurf",
ARCHIVED: "Archiviert",
};
const stateLabel = (s: string | undefined) => (s ? (STATE[s] ?? s) : "—");
// ── Shared error handler ──────────────────────────────────────────────────────
function handleError(error: unknown): { content: [{ type: "text"; text: string }]; isError: true } {
const msg = error instanceof Error ? error.message : String(error);
if (msg.includes("HTTP 401") || msg.includes("HTTP 403")) {
return {
isError: true,
content: [{ type: "text", text: `Fehler: Authentifizierung fehlgeschlagen. Bitte 'beam_get_token' aufrufen, um ein neues Token zu erhalten.\nDetails: ${msg}` }],
};
}
if (msg.includes("HTTP 404") || msg.includes("No factsheet found") || msg.includes("No domain found")) {
return {
isError: true,
content: [{ type: "text", text: `Fehler: Nicht gefunden. Bitte prüfe die ID oder den Suchbegriff.\nDetails: ${msg}` }],
};
}
if (msg.includes("HTTP 429")) {
return {
isError: true,
content: [{ type: "text", text: `Fehler: Rate-Limit erreicht. Bitte kurz warten und erneut versuchen.` }],
};
}
return {
isError: true,
content: [{ type: "text", text: `Fehler: ${msg}` }],
};
}
// ── Truncation helper ─────────────────────────────────────────────────────────
function truncateIfNeeded(text: string): string {
if (text.length <= CHARACTER_LIMIT) return text;
return text.slice(0, CHARACTER_LIMIT) + "\n\n⚠ Ausgabe wurde auf 25.000 Zeichen gekürzt. Nutze spezifischere Filter.";
}
// ── Tool: beam_get_token ──────────────────────────────────────────────────────
server.registerTool(
"beam_get_token",
{
title: "BEAM Token abrufen",
description: `Meldet sich automatisch bei BEAM an (headless Edge Browser) und hält das Token im Speicher (keine .env Persistenz).
Nur nötig wenn kein gültiges Token vorhanden ist oder ein 401/403-Fehler auftritt.
Returns:
Bestätigung mit Benutzername und Token-Ablaufzeit.
Examples:
- Nutze dieses Tool wenn andere BEAM-Tools mit Authentifizierungsfehlern fehlschlagen.
- Nutze es proaktiv vor der ersten BEAM-Abfrage.`,
inputSchema: {},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
async () => {
try {
const existing = await getToken().catch(() => null);
if (existing) {
return { content: [{ type: "text", text: "✓ Gültiges BEAM-Token bereits vorhanden. Kein Login nötig." }] };
}
} catch {
// kein Token — Login starten
}
try {
const token = await loginWithBrowser();
const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
const expiry = new Date((payload.exp ?? 0) * 1000).toLocaleString("de-DE");
const user = payload.principal?.username ?? "Unbekannt";
return {
content: [{
type: "text",
text: `✓ BEAM Login erfolgreich!\nBenutzer: ${user}\nToken gültig bis: ${expiry}\nToken wird nur im Speicher gehalten.`,
}],
};
} catch (error) {
return handleError(error);
}
}
);
// ── Tool: beam_search_factsheets ──────────────────────────────────────────────
server.registerTool(
"beam_search_factsheets",
{
title: "BEAM Fact Sheets suchen",
description: `Sucht nach BEAM Fact Sheets anhand von Name, Beam-ID (z.B. 'A-106614') oder Freitext.
Gibt bis zu 20 Ergebnisse zurück mit Typ, Lebenszyklus und ID.
Args:
- query (string, required): Suchbegriff, Beam-ID oder Name
- type (enum, optional): Einschränkung auf Fact-Sheet-Typ
Returns:
Liste der gefundenen Fact Sheets mit Typ, Name, Lebenszyklus und UUID.
Examples:
- query="A-106614" → Suche nach Beam-ID
- query="RiM", type="Application" → Anwendung nach Name suchen
- query="Personaleinsatz" → Freitextsuche über alle Typen`,
inputSchema: {
query: z.string()
.min(1, "Suchbegriff darf nicht leer sein")
.describe("Suchbegriff, Beam-ID (z.B. A-106614) oder Name"),
type: z.enum(["Application", "BusinessCapability", "ITComponent", "Interface", "DataObject"])
.optional()
.describe("Optional: Einschränkung auf einen Fact-Sheet-Typ"),
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
async ({ query, type }) => {
try {
const results = await searchFactSheets(query, type);
if (!results.length) {
return { content: [{ type: "text", text: `Keine Ergebnisse in BEAM für: ${query}` }] };
}
const text = results.map((r: Record<string, unknown>) => {
const extId = (r.externalId as Record<string, string> | null)?.externalId ?? "";
const lc = lcLabel((r.lifecycle as Record<string, string> | null)?.asString);
const alias = r.alias ? ` (${r.alias})` : "";
return `• [${typeLabel(r.type as string)}] ${extId ? extId + " - " : ""}${r.name}${alias} | ${lc} | ID: ${r.id}`;
}).join("\n");
return { content: [{ type: "text", text: truncateIfNeeded(`BEAM Suchergebnisse für "${query}":\n\n${text}`) }] };
} catch (error) {
return handleError(error);
}
}
);
// ── Tool: beam_get_factsheet ──────────────────────────────────────────────────
server.registerTool(
"beam_get_factsheet",
{
title: "BEAM Fact Sheet Details",
description: `Ruft vollständige Details eines BEAM Fact Sheets ab — per Beam-ID (z.B. 'A-106614') oder UUID.
Unterstützt Anwendungen, Domänen und IT-Komponenten mit Relationen.
Args:
- id (string, required): Beam-ID (z.B. A-106614, P.F.05.05.04) oder UUID
Returns:
Detaillierte Fact-Sheet-Informationen inkl. Beschreibung, Lebenszyklus, Tags,
verknüpfte Domänen, IT-Komponenten und Nachfolger.
Examples:
- id="A-106614" → Anwendung per Beam-ID
- id="P.F.05.05.04" → Domäne per externer ID
- id="33cb34f7-..." → Fact Sheet per UUID`,
inputSchema: {
id: z.string()
.min(1, "ID darf nicht leer sein")
.describe("Beam-ID (z.B. A-106614) oder UUID des Fact Sheets"),
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
async ({ id }) => {
try {
const fs = await getFactSheet(id) as Record<string, unknown>;
const lines: string[] = [];
const extId = (fs.externalId as Record<string, string> | null)?.externalId ?? "";
lines.push(`## ${extId ? extId + " - " : ""}${fs.name}`);
lines.push(`Typ: ${typeLabel(fs.type as string)}`);
lines.push(`Status: ${stateLabel(fs.lxState as string)}`);
if (fs.alias) lines.push(`Alias: ${fs.alias}`);
const lc = (fs.lifecycle as Record<string, string> | null)?.asString;
if (lc) lines.push(`Lebenszyklus: ${lcLabel(lc)}`);
if (fs.description) lines.push(`\nBeschreibung:\n${fs.description}`);
const tags = (fs.tags as { name: string }[] | null);
if (tags?.length) lines.push(`\nTags: ${tags.map(t => t.name).join(", ")}`);
// Domänen (für Anwendungen)
const domRel = fs.relApplicationToBusinessCapability as { edges: { node: Record<string, unknown> }[] } | null;
if (domRel?.edges?.length) {
lines.push("\n### Domänen");
domRel.edges.forEach(({ node }) => {
const dom = node.factSheet as Record<string, unknown>;
const domExtId = (dom.externalId as Record<string, string> | null)?.externalId ?? "";
lines.push(`${domExtId ? domExtId + " - " : ""}${dom.name} | Supporttyp: ${stLabel(node.supportType as string)}`);
});
}
// Anwendungen (für Domänen)
const appRel = fs.relBusinessCapabilityToApplication as { edges: { node: Record<string, unknown> }[] } | null;
if (appRel?.edges?.length) {
lines.push("\n### Anwendungen");
appRel.edges.forEach(({ node }) => {
const app = node.factSheet as Record<string, unknown>;
const appExtId = (app.externalId as Record<string, string> | null)?.externalId ?? "";
const appLc = lcLabel((app.lifecycle as Record<string, string> | null)?.asString);
lines.push(`${appExtId ? appExtId + " - " : ""}${app.name} | Supporttyp: ${stLabel(node.supportType as string)} | ${appLc}`);
});
}
// IT-Komponenten
const itcRel = fs.relApplicationToITComponent as { edges: { node: Record<string, unknown> }[] } | null;
if (itcRel?.edges?.length) {
lines.push("\n### IT-Komponenten");
itcRel.edges.forEach(({ node }) => {
lines.push(`${(node.factSheet as Record<string, unknown>).name}`);
});
}
// Nachfolger
const succRel = fs.relToSuccessor as { edges: { node: Record<string, unknown> }[] } | null;
if (succRel?.edges?.length) {
lines.push("\n### Nachfolger");
succRel.edges.forEach(({ node }) => {
const succ = node.factSheet as Record<string, unknown>;
lines.push(`${succ.name} (ID: ${succ.id})`);
});
}
return { content: [{ type: "text", text: truncateIfNeeded(lines.join("\n")) }] };
} catch (error) {
return handleError(error);
}
}
);
// ── Tool: beam_get_domain_applications ─────────────────────────────────────────
server.registerTool(
"beam_get_domain_applications",
{
title: "BEAM Domänen-Anwendungen",
description: `Listet alle Anwendungen einer BEAM-Domäne auf, gruppiert nach Supporttyp (Führend/Unterstützt).
Args:
- domain (string, required): Domänen-ID (z.B. 'P.F.05.05.04'), Name oder UUID
Returns:
Gruppierte Liste aller Anwendungen der Domäne mit Lebenszyklus und Tags.
Examples:
- domain="P.F.05.05.04" → Domäne per externer ID
- domain="Informationen bereitstellen" → Domäne per Name
- domain="TO.I.01.01" → Technische Domäne`,
inputSchema: {
domain: z.string()
.min(1, "Domäne darf nicht leer sein")
.describe("Domänen-ID (z.B. 'P.F.05.05.04'), Name oder UUID"),
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
async ({ domain }) => {
try {
const fs = await getDomainApplications(domain) as Record<string, unknown>;
const extId = (fs.externalId as Record<string, string> | null)?.externalId ?? "";
const lines: string[] = [];
lines.push(`## BEAM-Domäne: ${extId ? extId + " - " : ""}${fs.name}`);
const appRel = fs.relBusinessCapabilityToApplication as { edges: { node: Record<string, unknown> }[] } | null;
if (!appRel?.edges?.length) {
lines.push("Keine Anwendungen in dieser Domäne gefunden.");
return { content: [{ type: "text", text: lines.join("\n") }] };
}
lines.push(`${appRel.edges.length} Anwendung(en)\n`);
const grouped: Record<string, string[]> = {};
appRel.edges.forEach(({ node }) => {
const app = node.factSheet as Record<string, unknown>;
const appExtId = (app.externalId as Record<string, string> | null)?.externalId ?? "";
const appLc = lcLabel((app.lifecycle as Record<string, string> | null)?.asString);
const st = stLabel(node.supportType as string);
const appTags = (app.tags as { name: string }[] | null)?.map(t => t.name).join(", ") ?? "";
const entry = `${appExtId ? appExtId + " - " : ""}${app.name} | ${appLc}${appTags ? " | " + appTags : ""}`;
if (!grouped[st]) grouped[st] = [];
grouped[st].push(entry);
});
const order = ["Führend", "Unterstützt"];
const allTypes = [
...order.filter(k => grouped[k]),
...Object.keys(grouped).filter(k => !order.includes(k)),
];
allTypes.forEach((t) => {
lines.push(`### ${t} (${grouped[t].length})`);
grouped[t].forEach((e) => lines.push(e));
lines.push("");
});
return { content: [{ type: "text", text: truncateIfNeeded(lines.join("\n")) }] };
} catch (error) {
return handleError(error);
}
}
);
// ── Tool: beam_get_diagram ────────────────────────────────────────────────────
server.registerTool(
"beam_get_diagram",
{
title: "BEAM Diagramme abrufen",
description: `Ruft Diagramme einer BEAM-Anwendung ab. Ohne diagram_name werden alle verknüpften Diagramme aufgelistet.
Mit diagram_name wird eine lesbare Zusammenfassung (Systeme, Schnittstellen) angezeigt.
Optional als .drawio-Datei speichern zum Öffnen in draw.io.
Args:
- application (string, required): Anwendungsname, Beam-ID (z.B. 'A-109410') oder UUID
- diagram_name (string, optional): Name oder Teil des Diagrammnamens zum Abrufen
- save_path (string, optional): Dateipfad zum Speichern als .drawio-Datei
Returns:
Ohne diagram_name: Liste aller verfügbaren Diagramme.
Mit diagram_name: Lesbare Zusammenfassung mit Systemen/Akteuren und Schnittstellen.
Mit save_path: Zusätzlich wird eine .drawio-Datei gespeichert.
Examples:
- application="KEP_KEO_Revision" → Alle Diagramme auflisten
- application="A-109410", diagram_name="System Context" → Diagramm lesen
- application="A-109410", diagram_name="System Context", save_path="./diagram.drawio" → Als draw.io speichern`,
inputSchema: {
application: z.string()
.min(1, "Anwendung darf nicht leer sein")
.describe("Anwendungsname, Beam-ID (z.B. 'A-109410') oder UUID"),
diagram_name: z.string()
.optional()
.describe("Optional: Name oder Teil des Diagrammnamens zum Abrufen"),
save_path: z.string()
.optional()
.describe("Optional: Dateipfad zum Speichern als .drawio-Datei (z.B. './diagram.drawio')"),
},
annotations: {
readOnlyHint: false, // kann Dateien schreiben wenn save_path gesetzt
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
async ({ application, diagram_name, save_path }) => {
try {
const { factSheet, diagrams } = await getFactSheetDiagrams(application);
if (!diagrams.length) {
return { content: [{ type: "text", text: `Keine Diagramme bei ${factSheet.name} gefunden.` }] };
}
// Nur auflisten wenn kein diagram_name
if (!diagram_name) {
const lines = [`## Diagramme von ${factSheet.name}\n`];
diagrams.forEach((d) => {
lines.push(`${d.name} [${d.groupKey}]\n ID: ${d.id}`);
});
lines.push(`\nNutze diagram_name um ein Diagramm abzurufen.`);
lines.push(`Nutze save_path um ein Diagramm als .drawio-Datei zu speichern.`);
return { content: [{ type: "text", text: lines.join("\n") }] };
}
// Diagramm finden (Teilmatch)
const match = diagrams.find((d) =>
d.name.toLowerCase().includes(diagram_name.toLowerCase()) || d.id === diagram_name
);
if (!match) {
return {
content: [{
type: "text",
text: `Kein Diagramm gefunden für "${diagram_name}". Verfügbar:\n${diagrams.map((d) => `${d.name}`).join("\n")}`,
}],
};
}
const raw = await getDiagram(match.id);
const bm = raw.data;
const summary = parseDiagramToText(bm);
const contentParts: { type: "text"; text: string }[] = [{ type: "text", text: truncateIfNeeded(summary) }];
// Optional als .drawio speichern
const graphXml = extractGraphXml(bm);
if (save_path && graphXml) {
const filePath = saveDiagramAsDrawio(graphXml, (bm.name as string) ?? match.name, save_path);
contentParts.push({
type: "text",
text: `\n✓ Diagramm gespeichert: ${filePath}\nDie Datei kann direkt in draw.io geöffnet werden (Datei → Öffnen oder Doppelklick).`,
});
} else if (save_path && !graphXml) {
contentParts.push({
type: "text",
text: `\n⚠ Diagramm enthält kein graphXml — Speichern als .drawio nicht möglich.`,
});
}
return { content: contentParts };
} catch (error) {
return handleError(error);
}
}
);
// ── Start ─────────────────────────────────────────────────────────────────────
const transport = new StdioServerTransport();
await server.connect(transport);
+169
View File
@@ -0,0 +1,169 @@
/**
* Keychain-Modul: Sichere Speicherung von Credentials im OS-Keychain.
*
* - macOS: Keychain Access via `security` CLI
* - Windows: DPAPI-verschlüsselte Datei im AppData-Verzeichnis
* - Fallback: Kein persistenter Speicher (nur In-Memory)
*
* Keine externen Dependencies — nur Node.js Built-ins + OS-Tools.
*/
import { execSync } from "child_process";
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
import { join } from "path";
import { homedir, platform } from "os";
const SERVICE_NAME = "beam-mcp";
const ACCOUNT_NAME = "beam-token";
// ── Platform Detection ────────────────────────────────────────────────────────
const OS = platform();
// ── macOS Keychain via `security` CLI ─────────────────────────────────────────
function macosGet(): string | null {
try {
const result = execSync(
`security find-generic-password -a "${ACCOUNT_NAME}" -s "${SERVICE_NAME}" -w`,
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
);
return result.trim() || null;
} catch {
return null;
}
}
function macosSet(token: string): void {
// -U flag updates existing entry or creates new one
execSync(
`security add-generic-password -a "${ACCOUNT_NAME}" -s "${SERVICE_NAME}" -w "${token.replace(/"/g, '\\"')}" -U`,
{ stdio: ["pipe", "pipe", "pipe"] }
);
}
function macosDelete(): void {
try {
execSync(
`security delete-generic-password -a "${ACCOUNT_NAME}" -s "${SERVICE_NAME}"`,
{ stdio: ["pipe", "pipe", "pipe"] }
);
} catch {
// Ignore if entry doesn't exist
}
}
// ── Windows DPAPI-encrypted file ──────────────────────────────────────────────
function getWindowsStorePath(): string {
const appData = process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local");
const dir = join(appData, "beam-mcp");
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
return join(dir, "credentials.enc");
}
function windowsGet(): string | null {
const storePath = getWindowsStorePath();
if (!existsSync(storePath)) return null;
try {
const encrypted = readFileSync(storePath, "utf8");
// Decrypt using DPAPI via PowerShell
const psScript = `
$bytes = [System.Convert]::FromBase64String('${encrypted}')
$decrypted = [System.Security.Cryptography.ProtectedData]::Unprotect($bytes, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)
[System.Text.Encoding]::UTF8.GetString($decrypted)
`;
const result = execSync(
`powershell -NoProfile -NonInteractive -Command "${psScript.replace(/\n/g, " ")}"`,
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
);
return result.trim() || null;
} catch {
return null;
}
}
function windowsSet(token: string): void {
const storePath = getWindowsStorePath();
// Encrypt using DPAPI via PowerShell
const psScript = `
Add-Type -AssemblyName System.Security
$bytes = [System.Text.Encoding]::UTF8.GetBytes('${token.replace(/'/g, "''")}')
$encrypted = [System.Security.Cryptography.ProtectedData]::Protect($bytes, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)
[System.Convert]::ToBase64String($encrypted)
`;
const result = execSync(
`powershell -NoProfile -NonInteractive -Command "${psScript.replace(/\n/g, " ")}"`,
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
);
writeFileSync(storePath, result.trim(), "utf8");
}
function windowsDelete(): void {
const storePath = getWindowsStorePath();
try {
if (existsSync(storePath)) unlinkSync(storePath);
} catch {
// Ignore
}
}
// ── Public API ────────────────────────────────────────────────────────────────
/**
* Liest das gespeicherte Token aus dem OS-Keychain.
* Gibt null zurück wenn kein Token gespeichert ist oder das OS nicht unterstützt wird.
*/
export function getStoredToken(): string | null {
switch (OS) {
case "darwin":
return macosGet();
case "win32":
return windowsGet();
default:
process.stderr.write(`[BEAM] Keychain nicht unterstützt auf ${OS}. Nur In-Memory Token.\n`);
return null;
}
}
/**
* Speichert ein Token sicher im OS-Keychain.
* Überschreibt ein vorhandenes Token.
*/
export function storeToken(token: string): void {
switch (OS) {
case "darwin":
macosSet(token);
process.stderr.write("[BEAM] Token in macOS Keychain gespeichert.\n");
break;
case "win32":
windowsSet(token);
process.stderr.write("[BEAM] Token verschlüsselt gespeichert (Windows DPAPI).\n");
break;
default:
process.stderr.write(`[BEAM] Keychain nicht unterstützt auf ${OS}. Token nur im Speicher.\n`);
}
}
/**
* Löscht das gespeicherte Token aus dem OS-Keychain.
*/
export function deleteStoredToken(): void {
switch (OS) {
case "darwin":
macosDelete();
break;
case "win32":
windowsDelete();
break;
default:
break;
}
}
/**
* Prüft ob Keychain-Unterstützung auf diesem OS verfügbar ist.
*/
export function isKeychainSupported(): boolean {
return OS === "darwin" || OS === "win32";
}
+34
View File
@@ -0,0 +1,34 @@
/**
* Standalone Login-Skript
* Öffnet einen sichtbaren Edge-Browser, führt den BEAM-Login durch.
* Das Token wird im OS-Keychain gespeichert (macOS Keychain / Windows DPAPI).
*
* Verwendung: npm run login
*/
import { loginWithBrowser } from "./auth.js";
import { isKeychainSupported } from "./keychain.js";
console.log("🔐 BEAM Login wird gestartet...");
console.log(" Ein Edge-Browser-Fenster öffnet sich gleich.\n");
if (!isKeychainSupported()) {
console.warn("⚠ Keychain wird auf diesem OS nicht unterstützt.");
console.warn(" Token wird nur im Speicher gehalten (nicht persistent).\n");
}
try {
const token = await loginWithBrowser();
const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
const expiry = new Date((payload.exp ?? 0) * 1000).toLocaleString("de-DE");
const user = payload.principal?.username ?? payload.sub ?? "Unbekannt";
console.log("✅ Login erfolgreich!");
console.log(` Benutzer: ${user}`);
console.log(` Token gültig bis: ${expiry}`);
console.log(` Gespeichert in: OS-Keychain (${process.platform === "darwin" ? "macOS Keychain" : "Windows DPAPI"})`);
console.log("\n Der MCP-Server nutzt das Token automatisch beim nächsten Start.");
} catch (err) {
console.error("❌ Login fehlgeschlagen:", err instanceof Error ? err.message : err);
process.exit(1);
}
+13
View File
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}