From d881dab30dd53ae0dfa0f03d1ed14968d9873609 Mon Sep 17 00:00:00 2001 From: JiriUhlir <149317995+JiriUhlir@users.noreply.github.com> Date: Fri, 28 Aug 2026 10:21:48 +0200 Subject: [PATCH] MCP EasyWeb: srovnani adresy a srozumitelna hlaska u HTTP 400 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Overeno proti bezicimu serveru (samolepak klon z jejich settings.json): /login vraci 400 Bad Request na kazde udaje - spravne heslo, spatne heslo, prazdne heslo i neznamy ucet dostanou tutez odpoved, telo je vzdycky jen "Bad Request" v HTML. Bez hlavicky Authorization vraci 401, takze handler udaje cte a odmita je az uvnitr. Tvar pozadavku tedy sedi: totez posila jejich vlastni WPF klient a stejny 400 vraci i holy curl. Zmeny jsou proto v tom, aby z toho clovek poznal, co se deje: - 400 i 401 z /login hlasi jako odmitnute prihlaseni a rovnou rikaji, ze tenhle server odpovida stejnym kodem i na neznamy ucet. Driv to bylo holé "vratilo HTTP 400", coz posilalo hledat chybu v adrese - adresa se u EasyWebu srovnava jako v jejich klientovi: vlepena prihlasovaci cesta se odrizne a chybejici /mcp se doplni. Server je na cestu prisny, i /mcp/login/ s lomitkem vraci 404 - u obecne sluzby se adresa nemeni. Cizi server muze mit endpoint kdekoliv a "opravit" mu ji podle naseho odhadu znamena rozbit napojeni, ktere by jinak fungovalo Co-Authored-By: Claude Opus 5 (1M context) --- documentation/24-mcp-konektory.md | 22 +++++++++++++++ src/mcp/auth.ts | 22 ++++++++++++--- src/mcp/client.ts | 4 +-- src/mcp/dialect.ts | 47 +++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 6 deletions(-) diff --git a/documentation/24-mcp-konektory.md b/documentation/24-mcp-konektory.md index 3f967e0..c5b7d19 100644 --- a/documentation/24-mcp-konektory.md +++ b/documentation/24-mcp-konektory.md @@ -103,6 +103,28 @@ Otisk zarizeni se doplnuje z ID konektoru (`worknuke-con_abc123`), aby server poznal, ze jde porad o totez zarizeni, a aby si dve napojeni tehoz portalu nesahala do sezeni. +### Adresa se u EasyWebu srovnava + +Vlepena prihlasovaci cesta se odrizne a chybejici `/mcp` se doplni, takze +`.../centaur/mcp/login` i `.../CentaurWebApp` skonci tam, kde ma. Server je na +cestu prisny - i `/mcp/login/` s lomitkem na konci vraci 404. + +**U obecne sluzby se adresa nemeni.** Cizi server muze mit endpoint kdekoliv +a "opravit" mu adresu podle naseho odhadu znamena rozbit napojeni, ktere by +jinak fungovalo. + +### Kdyz server odpovi 400 + +`400 Bad Request` z `/login` **neni chyba pozadavku u nas**. Server ho ma jako +"malformed authorization request" a odpovida jim i na udaje, ktere neuzna. +Overeno proti bezicimu serveru: spatne heslo, prazdne heslo i neznamy ucet +vraceji tutez odpoved jako udaje spravne, telo je vzdycky jen `Bad Request` +v HTML. Rozlisit se to zvenku neda, takze to hlaska rika narovinu misto toho, +aby posilala cloveka hledat chybu v adrese. + +Rozdil oproti chybejicimu prihlaseni je videt: `/login` **bez** hlavicky +`Authorization` vraci 401, s ni uz 400. + ## Prihlaseni a zivotnost tokenu Cely zivotni cyklus resi `src/mcp/auth.ts`. **Token je kratkodoby, jeho diff --git a/src/mcp/auth.ts b/src/mcp/auth.ts index c633980..6d9e200 100644 --- a/src/mcp/auth.ts +++ b/src/mcp/auth.ts @@ -23,7 +23,7 @@ import { config } from '../config.js'; import type { ResolvedTarget } from '../scripts/connections.js'; import { truncate } from '../scripts/util.js'; -import { dialectFor } from './dialect.js'; +import { dialectFor, normalizeServerUrl } from './dialect.js'; /** * O kolik driv nez vyprsi se token vymeni. @@ -104,7 +104,9 @@ function credentialsOf(target: ResolvedTarget): Credentials { const value = (key: string): string => (target.serviceConfig[key] ?? '').trim(); return { serviceId: target.serviceId, - serverUrl: value('serverUrl'), + // Srovnana, aby prihlasovaci endpointy sedly i kdyz clovek vlepil adresu + // vcetne `/login` nebo bez `/mcp`. + serverUrl: normalizeServerUrl(target.serviceId, value('serverUrl')), token: value('token'), clientId: value('clientId'), clientSecret: value('clientSecret'), @@ -266,9 +268,21 @@ async function easyWebLogin(credentials: Credentials, signal: AbortSignal): Prom if (!response.ok) { const detail = truncate(await response.text(), config.errorDetailBytes); + /* + * 400 tady neni rozbity pozadavek u nas. + * + * Server ma `400 Bad Request` jako "malformed authorization request" + * a odpovida jim i na udaje, ktere neuzna - overeno proti bezicimu + * serveru, kde spatne heslo, prazdne heslo i neznamy ucet vraceji stejnou + * odpoved jako spravne udaje. Rozlisit se to zvenku neda, takze hlaska + * musi rict obojí, jinak clovek hleda chybu v adrese. + */ throw new AuthFailure( - response.status === 401 - ? `Server jméno a heslo nepřijal (HTTP 401 z ${url}).` + response.status === 401 || response.status === 400 + ? `Server přihlášení nepřijal (HTTP ${response.status} z ${url}). ` + + 'Ověřte jméno a heslo a to, že účet na tomhle serveru existuje. ' + + 'Tenhle server odpovídá stejným kódem i na neznámý účet, takže víc ' + + 'z odpovědi poznat nejde.' : `Přihlášení na ${url} vrátilo HTTP ${response.status}.`, detail === '' ? null : detail, ); diff --git a/src/mcp/client.ts b/src/mcp/client.ts index 6111bde..9e87f10 100644 --- a/src/mcp/client.ts +++ b/src/mcp/client.ts @@ -26,7 +26,7 @@ import { targetSecrets, type ResolvedTarget } from '../scripts/connections.js'; import { isPrivateHost } from '../scripts/http.js'; import { createRedactor, truncate } from '../scripts/util.js'; import { AuthFailure, authorize, forgetToken, type AuthMethod } from './auth.js'; -import { dialectFor, type McpDialect } from './dialect.js'; +import { dialectFor, normalizeServerUrl, type McpDialect } from './dialect.js'; /** * Verzi protokolu urcuje druh serveru (`dialect.ts`). @@ -120,7 +120,7 @@ class McpFailure extends Error { * nechat navazat spojeni na cokoliv, co je z containeru videt. */ function serverUrl(target: ResolvedTarget): URL { - const raw = (target.serviceConfig.serverUrl ?? '').trim(); + const raw = normalizeServerUrl(target.serviceId, target.serviceConfig.serverUrl ?? ''); if (raw === '') throw new McpFailure('Adresa MCP serveru není vyplněná.'); let url: URL; diff --git a/src/mcp/dialect.ts b/src/mcp/dialect.ts index 07d2a1e..b392a03 100644 --- a/src/mcp/dialect.ts +++ b/src/mcp/dialect.ts @@ -77,3 +77,50 @@ export function isMcpService(serviceId: string): boolean { export function dialectFor(serviceId: string): McpDialect { return dialects[serviceId] ?? dialects[MCP_SERVICE_ID]; } + +/** + * Cesty, ktere u EasyWebu patri prihlaseni, ne samotnemu MCP. + * + * Kdyz je nekdo vlepi do adresy serveru, je to pochopitelna zamena: v + * dokumentaci serveru jsou hned vedle sebe. Odriznou se. + */ +const AUTH_SUFFIXES = ['/login', '/logout', '/renew-access-token', '/renew-refresh-token']; + +/** + * Srovna adresu serveru do tvaru, ktery server ceka. + * + * **Jen u EasyWebu.** Ten ma pevnou cestu `/mcp` a vedle ni prihlasovaci + * endpointy, takze jde poznat, co je spravne, a spravit dve nejcastejsi + * zameny: vlepenou prihlasovaci cestu a chybejici `/mcp` na konci. Server je + * na to prisny - `/mcp/login/` s lomitkem uz vraci 404. + * + * U obecne sluzby se **nemeni nic**. Cizi server muze mit endpoint kdekoliv + * a "opravit" mu adresu podle naseho odhadu znamena rozbit napojeni, ktere by + * jinak fungovalo. + */ +export function normalizeServerUrl(serviceId: string, raw: string): string { + const trimmed = raw.trim(); + if (dialectFor(serviceId).auth !== 'easyweb') return trimmed; + + let url: URL; + try { + url = new URL(trimmed); + } catch { + // Neplatnou adresu resi az volajici, tady se nehada. + return trimmed; + } + + let path = url.pathname.replace(/\/+$/, ''); + for (const suffix of AUTH_SUFFIXES) { + if (path.toLowerCase().endsWith(suffix)) { + path = path.slice(0, -suffix.length); + break; + } + } + if (!path.toLowerCase().endsWith('/mcp')) path = `${path}/mcp`; + + url.pathname = path; + url.search = ''; + url.hash = ''; + return url.toString().replace(/\/+$/, ''); +}