MCP EasyWeb: srovnani adresy a srozumitelna hlaska u HTTP 400

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) <noreply@anthropic.com>
This commit is contained in:
JiriUhlir
2026-08-28 10:21:48 +02:00
co-authored by Claude Opus 5
parent 81e4348ad8
commit d881dab30d
4 changed files with 89 additions and 6 deletions
+18 -4
View File
@@ -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,
);
+2 -2
View File
@@ -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;
+47
View File
@@ -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(/\/+$/, '');
}