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:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,2 @@
|
||||
^[everything]
|
||||
* @FlorianHofmann
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
version: v3
|
||||
confidentiality: internal
|
||||
contacts: florian.hofmann@deutschebahn.com
|
||||
license: LicenseRef-DBPROPRIETARY
|
||||
reference-ids: none
|
||||
@@ -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(/&/g, "&").replace(/ /g, " ").replace(/<[^>]*>/g, ""),
|
||||
type: match[2],
|
||||
desc: match[3].replace(/&/g, "&").replace(/ /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(/&/g, "&").replace(/ /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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user