@inspectr/mcplab 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +762 -0
- package/dist/app/android-chrome-192x192.png +0 -0
- package/dist/app/android-chrome-512x512.png +0 -0
- package/dist/app/apple-touch-icon.png +0 -0
- package/dist/app/assets/index-DT-Z4AVG.js +249 -0
- package/dist/app/assets/index-EP4WAY8u.css +1 -0
- package/dist/app/favicon-16x16.png +0 -0
- package/dist/app/favicon-32x32.png +0 -0
- package/dist/app/favicon.svg +13 -0
- package/dist/app/index.html +26 -0
- package/dist/app/inspectr_logo_color.svg +9 -0
- package/dist/app/mcp.png +0 -0
- package/dist/app/mcp.svg +1 -0
- package/dist/app/robots.txt +14 -0
- package/dist/app/site.webmanifest +1 -0
- package/dist/app-server/app-context.d.ts +164 -0
- package/dist/app-server/app-context.d.ts.map +1 -0
- package/dist/app-server/app-context.js +2 -0
- package/dist/app-server/app-context.js.map +1 -0
- package/dist/app-server/assistant-common.d.ts +41 -0
- package/dist/app-server/assistant-common.d.ts.map +1 -0
- package/dist/app-server/assistant-common.js +104 -0
- package/dist/app-server/assistant-common.js.map +1 -0
- package/dist/app-server/config-store.d.ts +15 -0
- package/dist/app-server/config-store.d.ts.map +1 -0
- package/dist/app-server/config-store.js +67 -0
- package/dist/app-server/config-store.js.map +1 -0
- package/dist/app-server/dev-mcp.d.ts +6 -0
- package/dist/app-server/dev-mcp.d.ts.map +1 -0
- package/dist/app-server/dev-mcp.js +71 -0
- package/dist/app-server/dev-mcp.js.map +1 -0
- package/dist/app-server/evals-routes.d.ts +22 -0
- package/dist/app-server/evals-routes.d.ts.map +1 -0
- package/dist/app-server/evals-routes.js +135 -0
- package/dist/app-server/evals-routes.js.map +1 -0
- package/dist/app-server/http.d.ts +5 -0
- package/dist/app-server/http.d.ts.map +1 -0
- package/dist/app-server/http.js +31 -0
- package/dist/app-server/http.js.map +1 -0
- package/dist/app-server/index.d.ts +3 -0
- package/dist/app-server/index.d.ts.map +1 -0
- package/dist/app-server/index.js +2 -0
- package/dist/app-server/index.js.map +1 -0
- package/dist/app-server/jobs.d.ts +15 -0
- package/dist/app-server/jobs.d.ts.map +1 -0
- package/dist/app-server/jobs.js +11 -0
- package/dist/app-server/jobs.js.map +1 -0
- package/dist/app-server/libraries-store.d.ts +15 -0
- package/dist/app-server/libraries-store.d.ts.map +1 -0
- package/dist/app-server/libraries-store.js +61 -0
- package/dist/app-server/libraries-store.js.map +1 -0
- package/dist/app-server/markdown-reports.d.ts +12 -0
- package/dist/app-server/markdown-reports.d.ts.map +1 -0
- package/dist/app-server/markdown-reports.js +145 -0
- package/dist/app-server/markdown-reports.js.map +1 -0
- package/dist/app-server/oauth-debugger-domain.d.ts +230 -0
- package/dist/app-server/oauth-debugger-domain.d.ts.map +1 -0
- package/dist/app-server/oauth-debugger-domain.js +1098 -0
- package/dist/app-server/oauth-debugger-domain.js.map +1 -0
- package/dist/app-server/oauth-debugger.d.ts +20 -0
- package/dist/app-server/oauth-debugger.d.ts.map +1 -0
- package/dist/app-server/oauth-debugger.js +193 -0
- package/dist/app-server/oauth-debugger.js.map +1 -0
- package/dist/app-server/provider-models.d.ts +8 -0
- package/dist/app-server/provider-models.d.ts.map +1 -0
- package/dist/app-server/provider-models.js +60 -0
- package/dist/app-server/provider-models.js.map +1 -0
- package/dist/app-server/result-assistant-domain.d.ts +87 -0
- package/dist/app-server/result-assistant-domain.d.ts.map +1 -0
- package/dist/app-server/result-assistant-domain.js +212 -0
- package/dist/app-server/result-assistant-domain.js.map +1 -0
- package/dist/app-server/result-assistant.d.ts +22 -0
- package/dist/app-server/result-assistant.d.ts.map +1 -0
- package/dist/app-server/result-assistant.js +328 -0
- package/dist/app-server/result-assistant.js.map +1 -0
- package/dist/app-server/router.d.ts +4 -0
- package/dist/app-server/router.d.ts.map +1 -0
- package/dist/app-server/router.js +374 -0
- package/dist/app-server/router.js.map +1 -0
- package/dist/app-server/runs-routes.d.ts +44 -0
- package/dist/app-server/runs-routes.d.ts.map +1 -0
- package/dist/app-server/runs-routes.js +555 -0
- package/dist/app-server/runs-routes.js.map +1 -0
- package/dist/app-server/runs-store.d.ts +23 -0
- package/dist/app-server/runs-store.d.ts.map +1 -0
- package/dist/app-server/runs-store.js +84 -0
- package/dist/app-server/runs-store.js.map +1 -0
- package/dist/app-server/scenario-assistant-domain.d.ts +162 -0
- package/dist/app-server/scenario-assistant-domain.d.ts.map +1 -0
- package/dist/app-server/scenario-assistant-domain.js +269 -0
- package/dist/app-server/scenario-assistant-domain.js.map +1 -0
- package/dist/app-server/scenario-assistant.d.ts +29 -0
- package/dist/app-server/scenario-assistant.d.ts.map +1 -0
- package/dist/app-server/scenario-assistant.js +246 -0
- package/dist/app-server/scenario-assistant.js.map +1 -0
- package/dist/app-server/settings-store.d.ts +4 -0
- package/dist/app-server/settings-store.d.ts.map +1 -0
- package/dist/app-server/settings-store.js +32 -0
- package/dist/app-server/settings-store.js.map +1 -0
- package/dist/app-server/snapshots-routes.d.ts +24 -0
- package/dist/app-server/snapshots-routes.d.ts.map +1 -0
- package/dist/app-server/snapshots-routes.js +82 -0
- package/dist/app-server/snapshots-routes.js.map +1 -0
- package/dist/app-server/static-serving.d.ts +17 -0
- package/dist/app-server/static-serving.d.ts.map +1 -0
- package/dist/app-server/static-serving.js +64 -0
- package/dist/app-server/static-serving.js.map +1 -0
- package/dist/app-server/store-utils.d.ts +5 -0
- package/dist/app-server/store-utils.d.ts.map +1 -0
- package/dist/app-server/store-utils.js +26 -0
- package/dist/app-server/store-utils.js.map +1 -0
- package/dist/app-server/tool-analysis-domain.d.ts +146 -0
- package/dist/app-server/tool-analysis-domain.d.ts.map +1 -0
- package/dist/app-server/tool-analysis-domain.js +556 -0
- package/dist/app-server/tool-analysis-domain.js.map +1 -0
- package/dist/app-server/tool-analysis-storage.d.ts +41 -0
- package/dist/app-server/tool-analysis-storage.d.ts.map +1 -0
- package/dist/app-server/tool-analysis-storage.js +110 -0
- package/dist/app-server/tool-analysis-storage.js.map +1 -0
- package/dist/app-server/tool-analysis.d.ts +22 -0
- package/dist/app-server/tool-analysis.d.ts.map +1 -0
- package/dist/app-server/tool-analysis.js +271 -0
- package/dist/app-server/tool-analysis.js.map +1 -0
- package/dist/app-server/types.d.ts +28 -0
- package/dist/app-server/types.d.ts.map +1 -0
- package/dist/app-server/types.js +2 -0
- package/dist/app-server/types.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +544 -0
- package/dist/cli.js.map +1 -0
- package/dist/snapshot.d.ts +80 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +401 -0
- package/dist/snapshot.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,1098 @@
|
|
|
1
|
+
import { randomBytes, createHash } from 'node:crypto';
|
|
2
|
+
import { URL } from 'node:url';
|
|
3
|
+
import { addJobEvent } from './jobs.js';
|
|
4
|
+
const SESSION_TTL_MS = 30 * 60 * 1000;
|
|
5
|
+
const SPEC_BASE = 'https://modelcontextprotocol.io/specification/draft/basic/authorization';
|
|
6
|
+
function nowIso() {
|
|
7
|
+
return new Date().toISOString();
|
|
8
|
+
}
|
|
9
|
+
function makeId(prefix) {
|
|
10
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
11
|
+
}
|
|
12
|
+
function toBase64Url(buffer) {
|
|
13
|
+
return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
14
|
+
}
|
|
15
|
+
function pkcePair() {
|
|
16
|
+
const verifier = toBase64Url(randomBytes(32));
|
|
17
|
+
const challenge = createHash('sha256').update(verifier).digest('base64url');
|
|
18
|
+
return { verifier, challenge, method: 'S256' };
|
|
19
|
+
}
|
|
20
|
+
function normalizeRuntime(runtime) {
|
|
21
|
+
return {
|
|
22
|
+
redirectMode: runtime?.redirectMode ?? 'local_callback',
|
|
23
|
+
usePkce: runtime?.usePkce !== false,
|
|
24
|
+
codeChallengeMethod: 'S256',
|
|
25
|
+
scopes: runtime?.scopes ?? [],
|
|
26
|
+
resource: runtime?.resource,
|
|
27
|
+
state: runtime?.state,
|
|
28
|
+
nonce: runtime?.nonce,
|
|
29
|
+
extraAuthParams: runtime?.extraAuthParams ?? {}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function baseSteps(method) {
|
|
33
|
+
const steps = [
|
|
34
|
+
[
|
|
35
|
+
'resolve_target_metadata',
|
|
36
|
+
'Resolve target metadata',
|
|
37
|
+
'Resolve resource and authorization server metadata and endpoints.'
|
|
38
|
+
]
|
|
39
|
+
];
|
|
40
|
+
if (method === 'cimd') {
|
|
41
|
+
steps.push([
|
|
42
|
+
'fetch_cimd',
|
|
43
|
+
'Fetch + validate Client ID Metadata Document',
|
|
44
|
+
'Fetch the client metadata document and validate required fields.'
|
|
45
|
+
]);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
steps.push([
|
|
49
|
+
'resolve_registration_source',
|
|
50
|
+
'Resolve/validate client registration source',
|
|
51
|
+
'Validate client information for the selected registration method.'
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
if (method === 'dcr') {
|
|
55
|
+
steps.push([
|
|
56
|
+
'dynamic_client_registration',
|
|
57
|
+
'Dynamic Client Registration',
|
|
58
|
+
'Register a client dynamically and validate the registration response.'
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
steps.push([
|
|
62
|
+
'build_authorization_request',
|
|
63
|
+
'Build authorization request',
|
|
64
|
+
'Construct the authorization request URL and validate parameters.'
|
|
65
|
+
], [
|
|
66
|
+
'browser_authorization',
|
|
67
|
+
'Browser authorization step',
|
|
68
|
+
'Open the authorization URL and authenticate/authorize the client.'
|
|
69
|
+
], [
|
|
70
|
+
'receive_authorization_response',
|
|
71
|
+
'Receive authorization response',
|
|
72
|
+
'Capture the redirect callback via local callback or manual paste.'
|
|
73
|
+
], [
|
|
74
|
+
'validate_callback',
|
|
75
|
+
'Validate state and callback semantics',
|
|
76
|
+
'Validate state, code, and authorization response semantics.'
|
|
77
|
+
], [
|
|
78
|
+
'token_exchange',
|
|
79
|
+
'Token exchange',
|
|
80
|
+
'Exchange authorization code for tokens and inspect the token response.'
|
|
81
|
+
], [
|
|
82
|
+
'token_validation',
|
|
83
|
+
'Token response validation',
|
|
84
|
+
'Validate token response fields and protocol expectations.'
|
|
85
|
+
], [
|
|
86
|
+
'resource_probe',
|
|
87
|
+
'Protected resource / MCP probe',
|
|
88
|
+
'Optionally call a protected endpoint with the access token.'
|
|
89
|
+
], [
|
|
90
|
+
'summary',
|
|
91
|
+
'Summary + compliance checks',
|
|
92
|
+
'Summarize validations, failures, and remediation tips.'
|
|
93
|
+
]);
|
|
94
|
+
return steps.map(([id, title, description]) => ({
|
|
95
|
+
id,
|
|
96
|
+
title,
|
|
97
|
+
description,
|
|
98
|
+
status: 'pending',
|
|
99
|
+
networkExchangeIds: [],
|
|
100
|
+
validationIds: []
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
function step(session, stepId) {
|
|
104
|
+
const found = session.steps.find((s) => s.id === stepId);
|
|
105
|
+
if (!found)
|
|
106
|
+
throw new Error(`Unknown OAuth debugger step: ${stepId}`);
|
|
107
|
+
return found;
|
|
108
|
+
}
|
|
109
|
+
function emitEvent(session, type, payload) {
|
|
110
|
+
addJobEvent(session, {
|
|
111
|
+
type,
|
|
112
|
+
ts: nowIso(),
|
|
113
|
+
payload
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function markStepStarted(session, stepId) {
|
|
117
|
+
const s = step(session, stepId);
|
|
118
|
+
if (s.status === 'completed' || s.status === 'failed')
|
|
119
|
+
return;
|
|
120
|
+
s.status = 'active';
|
|
121
|
+
s.startedAt = s.startedAt ?? nowIso();
|
|
122
|
+
session.updatedAt = Date.now();
|
|
123
|
+
emitEvent(session, 'step_started', { stepId, title: s.title });
|
|
124
|
+
}
|
|
125
|
+
function markStepCompleted(session, stepId, outcomeSummary) {
|
|
126
|
+
const s = step(session, stepId);
|
|
127
|
+
s.status = 'completed';
|
|
128
|
+
s.finishedAt = nowIso();
|
|
129
|
+
if (outcomeSummary)
|
|
130
|
+
s.outcomeSummary = outcomeSummary;
|
|
131
|
+
session.updatedAt = Date.now();
|
|
132
|
+
emitEvent(session, 'step_completed', {
|
|
133
|
+
stepId,
|
|
134
|
+
title: s.title,
|
|
135
|
+
outcomeSummary: outcomeSummary ?? null
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function markStepFailed(session, stepId, message) {
|
|
139
|
+
const s = step(session, stepId);
|
|
140
|
+
s.status = 'failed';
|
|
141
|
+
s.finishedAt = nowIso();
|
|
142
|
+
s.outcomeSummary = message;
|
|
143
|
+
session.updatedAt = Date.now();
|
|
144
|
+
emitEvent(session, 'step_failed', { stepId, title: s.title, message });
|
|
145
|
+
}
|
|
146
|
+
function markStepSkipped(session, stepId, reason) {
|
|
147
|
+
const s = step(session, stepId);
|
|
148
|
+
s.status = 'skipped';
|
|
149
|
+
s.finishedAt = nowIso();
|
|
150
|
+
s.outcomeSummary = reason;
|
|
151
|
+
}
|
|
152
|
+
function addValidation(session, finding) {
|
|
153
|
+
const id = makeId('ov');
|
|
154
|
+
const full = { id, ...finding };
|
|
155
|
+
session.validations.push(full);
|
|
156
|
+
const s = session.steps.find((stepItem) => stepItem.id === finding.stepId);
|
|
157
|
+
if (s)
|
|
158
|
+
s.validationIds.push(id);
|
|
159
|
+
emitEvent(session, 'validation', {
|
|
160
|
+
id,
|
|
161
|
+
stepId: finding.stepId,
|
|
162
|
+
severity: finding.severity,
|
|
163
|
+
code: finding.code,
|
|
164
|
+
title: finding.title
|
|
165
|
+
});
|
|
166
|
+
return full;
|
|
167
|
+
}
|
|
168
|
+
function addSequence(session, event) {
|
|
169
|
+
session.sequence.push({ id: makeId('seq'), ts: nowIso(), ...event });
|
|
170
|
+
}
|
|
171
|
+
function headersToObject(headers) {
|
|
172
|
+
const out = {};
|
|
173
|
+
headers.forEach((value, key) => {
|
|
174
|
+
out[key] = value;
|
|
175
|
+
});
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
function recordHttp(session, exchange) {
|
|
179
|
+
const id = makeId('http');
|
|
180
|
+
const full = {
|
|
181
|
+
id,
|
|
182
|
+
kind: 'http',
|
|
183
|
+
timestamp: nowIso(),
|
|
184
|
+
...exchange
|
|
185
|
+
};
|
|
186
|
+
session.network.push(full);
|
|
187
|
+
const s = session.steps.find((stepItem) => stepItem.id === exchange.stepId);
|
|
188
|
+
if (s)
|
|
189
|
+
s.networkExchangeIds.push(id);
|
|
190
|
+
emitEvent(session, full.phase === 'request' ? 'http_request' : 'http_response', {
|
|
191
|
+
id: full.id,
|
|
192
|
+
stepId: full.stepId,
|
|
193
|
+
label: full.label,
|
|
194
|
+
method: full.method ?? null,
|
|
195
|
+
url: full.url,
|
|
196
|
+
status: full.status ?? null
|
|
197
|
+
});
|
|
198
|
+
return full;
|
|
199
|
+
}
|
|
200
|
+
async function fetchWithTrace(params) {
|
|
201
|
+
const { session, stepId, label, url, method = 'GET', headers = {}, bodyText, timeoutMs = 15_000 } = params;
|
|
202
|
+
recordHttp(session, {
|
|
203
|
+
stepId,
|
|
204
|
+
phase: 'request',
|
|
205
|
+
label,
|
|
206
|
+
method,
|
|
207
|
+
url,
|
|
208
|
+
headers,
|
|
209
|
+
bodyText,
|
|
210
|
+
sensitiveFields: []
|
|
211
|
+
});
|
|
212
|
+
addSequence(session, {
|
|
213
|
+
from: 'Debugger',
|
|
214
|
+
to: label.toLowerCase().includes('token')
|
|
215
|
+
? 'Token Endpoint'
|
|
216
|
+
: label.toLowerCase().includes('probe')
|
|
217
|
+
? 'MCP/Resource'
|
|
218
|
+
: 'Auth Server',
|
|
219
|
+
label,
|
|
220
|
+
stepId
|
|
221
|
+
});
|
|
222
|
+
const controller = new AbortController();
|
|
223
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
224
|
+
const startedAt = Date.now();
|
|
225
|
+
try {
|
|
226
|
+
const response = await fetch(url, {
|
|
227
|
+
method,
|
|
228
|
+
headers,
|
|
229
|
+
body: bodyText,
|
|
230
|
+
signal: controller.signal
|
|
231
|
+
});
|
|
232
|
+
const responseText = await response.text();
|
|
233
|
+
let responseJson;
|
|
234
|
+
try {
|
|
235
|
+
responseJson = responseText ? JSON.parse(responseText) : undefined;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// non-json response
|
|
239
|
+
}
|
|
240
|
+
recordHttp(session, {
|
|
241
|
+
stepId,
|
|
242
|
+
phase: 'response',
|
|
243
|
+
label,
|
|
244
|
+
url,
|
|
245
|
+
headers: headersToObject(response.headers),
|
|
246
|
+
status: response.status,
|
|
247
|
+
bodyText: responseText,
|
|
248
|
+
durationMs: Date.now() - startedAt,
|
|
249
|
+
sensitiveFields: []
|
|
250
|
+
});
|
|
251
|
+
return { response, responseText, responseJson };
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
clearTimeout(timer);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function requiredString(value, error) {
|
|
258
|
+
const text = typeof value === 'string' ? value.trim() : '';
|
|
259
|
+
if (!text)
|
|
260
|
+
throw new Error(error);
|
|
261
|
+
return text;
|
|
262
|
+
}
|
|
263
|
+
function inferResourceMetadataUrl(baseUrl) {
|
|
264
|
+
const u = new URL(baseUrl);
|
|
265
|
+
u.pathname = '/.well-known/oauth-protected-resource';
|
|
266
|
+
u.search = '';
|
|
267
|
+
return u.toString();
|
|
268
|
+
}
|
|
269
|
+
function inferAuthServerMetadataUrl(issuerOrBase) {
|
|
270
|
+
const u = new URL(issuerOrBase);
|
|
271
|
+
u.pathname = '/.well-known/oauth-authorization-server';
|
|
272
|
+
u.search = '';
|
|
273
|
+
return u.toString();
|
|
274
|
+
}
|
|
275
|
+
function localCallbackUrl(session, appBaseUrl) {
|
|
276
|
+
return `${appBaseUrl.replace(/\/$/, '')}/api/oauth-debugger/sessions/${session.id}/callback`;
|
|
277
|
+
}
|
|
278
|
+
function buildAuthorizationUrl(session) {
|
|
279
|
+
const authEndpoint = session.config.target.overrides?.authorizationEndpoint ||
|
|
280
|
+
session.context.authServerMetadata?.authorization_endpoint;
|
|
281
|
+
if (!authEndpoint)
|
|
282
|
+
throw new Error('Authorization endpoint not resolved');
|
|
283
|
+
const resolvedClient = session.context.resolvedClient;
|
|
284
|
+
if (!resolvedClient?.clientId)
|
|
285
|
+
throw new Error('Client not resolved');
|
|
286
|
+
const callbackUrl = requiredString(session.context.callbackUrl, 'Callback URL not set');
|
|
287
|
+
const state = session.config.runtime.state || toBase64Url(randomBytes(16));
|
|
288
|
+
session.config.runtime.state = state;
|
|
289
|
+
const params = new URLSearchParams({
|
|
290
|
+
response_type: 'code',
|
|
291
|
+
client_id: resolvedClient.clientId,
|
|
292
|
+
redirect_uri: callbackUrl,
|
|
293
|
+
state
|
|
294
|
+
});
|
|
295
|
+
if ((session.config.runtime.scopes ?? []).length > 0) {
|
|
296
|
+
params.set('scope', (session.config.runtime.scopes ?? []).join(' '));
|
|
297
|
+
}
|
|
298
|
+
if (session.config.runtime.resource) {
|
|
299
|
+
params.set('resource', session.config.runtime.resource);
|
|
300
|
+
}
|
|
301
|
+
if (session.config.runtime.usePkce) {
|
|
302
|
+
session.context.pkce = session.context.pkce ?? pkcePair();
|
|
303
|
+
params.set('code_challenge', session.context.pkce.challenge);
|
|
304
|
+
params.set('code_challenge_method', session.context.pkce.method);
|
|
305
|
+
}
|
|
306
|
+
if (session.config.runtime.nonce)
|
|
307
|
+
params.set('nonce', session.config.runtime.nonce);
|
|
308
|
+
for (const [key, value] of Object.entries(session.config.runtime.extraAuthParams ?? {})) {
|
|
309
|
+
if (value != null && `${value}` !== '')
|
|
310
|
+
params.set(key, String(value));
|
|
311
|
+
}
|
|
312
|
+
const url = new URL(authEndpoint);
|
|
313
|
+
url.search = params.toString();
|
|
314
|
+
session.context.authorizationRequestUrl = url.toString();
|
|
315
|
+
return url.toString();
|
|
316
|
+
}
|
|
317
|
+
function parseCallbackInput(input) {
|
|
318
|
+
if (input.redirectUrl) {
|
|
319
|
+
const parsed = new URL(input.redirectUrl);
|
|
320
|
+
return {
|
|
321
|
+
rawUrl: input.redirectUrl,
|
|
322
|
+
code: parsed.searchParams.get('code') ?? undefined,
|
|
323
|
+
state: parsed.searchParams.get('state') ?? undefined,
|
|
324
|
+
error: parsed.searchParams.get('error') ?? undefined,
|
|
325
|
+
errorDescription: parsed.searchParams.get('error_description') ?? undefined
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
code: input.code,
|
|
330
|
+
state: input.state
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function resolvedClientFromConfig(session) {
|
|
334
|
+
if (session.config.registrationMethod === 'pre_registered') {
|
|
335
|
+
const c = session.config.clientConfig.preRegistered;
|
|
336
|
+
if (!c?.clientId)
|
|
337
|
+
throw new Error('pre-registered client_id is required');
|
|
338
|
+
session.context.resolvedClient = {
|
|
339
|
+
clientId: c.clientId,
|
|
340
|
+
clientSecret: c.clientSecret,
|
|
341
|
+
tokenEndpointAuthMethod: c.tokenEndpointAuthMethod
|
|
342
|
+
};
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (session.config.registrationMethod === 'cimd') {
|
|
346
|
+
const reg = session.context.registration;
|
|
347
|
+
const clientId = reg?.client_id ?? session.config.clientConfig.cimd?.expectedClientId;
|
|
348
|
+
if (!clientId)
|
|
349
|
+
throw new Error('CIMD did not provide client_id and no expectedClientId was set');
|
|
350
|
+
session.context.resolvedClient = {
|
|
351
|
+
clientId,
|
|
352
|
+
tokenEndpointAuthMethod: reg?.token_endpoint_auth_method ?? session.config.clientConfig.cimd?.expectedClientId
|
|
353
|
+
};
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
// dcr handled after registration call
|
|
357
|
+
}
|
|
358
|
+
async function stepResolveTargetMetadata(session) {
|
|
359
|
+
const stepId = 'resolve_target_metadata';
|
|
360
|
+
markStepStarted(session, stepId);
|
|
361
|
+
const server = session.serverConfig;
|
|
362
|
+
if (!server)
|
|
363
|
+
throw new Error(`MCP server '${session.config.target.serverName}' not found`);
|
|
364
|
+
const resourceMetadataUrl = session.config.target.overrides?.authorizationServerMetadataUrl
|
|
365
|
+
? undefined
|
|
366
|
+
: inferResourceMetadataUrl(session.config.target.overrides?.resourceBaseUrl || server.url);
|
|
367
|
+
if (resourceMetadataUrl) {
|
|
368
|
+
try {
|
|
369
|
+
const { response, responseJson, responseText } = await fetchWithTrace({
|
|
370
|
+
session,
|
|
371
|
+
stepId,
|
|
372
|
+
label: 'Protected Resource Metadata',
|
|
373
|
+
url: resourceMetadataUrl
|
|
374
|
+
});
|
|
375
|
+
if (!response.ok) {
|
|
376
|
+
addValidation(session, {
|
|
377
|
+
stepId,
|
|
378
|
+
severity: 'warning',
|
|
379
|
+
code: 'resource_metadata_fetch_failed',
|
|
380
|
+
title: 'Resource metadata fetch failed',
|
|
381
|
+
detail: `Protected resource metadata returned HTTP ${response.status}.`,
|
|
382
|
+
recommendation: 'Provide manual endpoint overrides or verify the protected resource metadata URL.'
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
session.context.resourceMetadata = responseJson ?? { raw: responseText };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
addValidation(session, {
|
|
391
|
+
stepId,
|
|
392
|
+
severity: 'warning',
|
|
393
|
+
code: 'resource_metadata_unreachable',
|
|
394
|
+
title: 'Protected resource metadata unreachable',
|
|
395
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
396
|
+
recommendation: 'Check the MCP server URL and network connectivity, or use manual endpoint overrides.'
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const authMetadataUrl = session.config.target.overrides?.authorizationServerMetadataUrl ||
|
|
401
|
+
(session.context.resourceMetadata?.authorization_servers?.[0]
|
|
402
|
+
? inferAuthServerMetadataUrl(String(session.context.resourceMetadata.authorization_servers[0]))
|
|
403
|
+
: session.context.resourceMetadata?.authorization_server
|
|
404
|
+
? inferAuthServerMetadataUrl(String(session.context.resourceMetadata.authorization_server))
|
|
405
|
+
: undefined);
|
|
406
|
+
if (authMetadataUrl) {
|
|
407
|
+
const { response, responseJson, responseText } = await fetchWithTrace({
|
|
408
|
+
session,
|
|
409
|
+
stepId,
|
|
410
|
+
label: 'Authorization Server Metadata',
|
|
411
|
+
url: authMetadataUrl
|
|
412
|
+
});
|
|
413
|
+
if (!response.ok) {
|
|
414
|
+
throw new Error(`Authorization server metadata request failed (${response.status})`);
|
|
415
|
+
}
|
|
416
|
+
session.context.authServerMetadata = responseJson ?? { raw: responseText };
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
session.context.authServerMetadata = {};
|
|
420
|
+
addValidation(session, {
|
|
421
|
+
stepId,
|
|
422
|
+
severity: 'warning',
|
|
423
|
+
code: 'auth_metadata_missing',
|
|
424
|
+
title: 'Authorization metadata URL not discovered',
|
|
425
|
+
detail: 'Could not derive authorization server metadata URL automatically from the selected MCP server.',
|
|
426
|
+
recommendation: 'Use Advanced overrides to set authorization/token/registration endpoints.'
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
if (session.config.target.overrides?.authorizationEndpoint) {
|
|
430
|
+
session.context.authServerMetadata = {
|
|
431
|
+
...(session.context.authServerMetadata ?? {}),
|
|
432
|
+
authorization_endpoint: session.config.target.overrides.authorizationEndpoint
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
if (session.config.target.overrides?.tokenEndpoint) {
|
|
436
|
+
session.context.authServerMetadata = {
|
|
437
|
+
...(session.context.authServerMetadata ?? {}),
|
|
438
|
+
token_endpoint: session.config.target.overrides.tokenEndpoint
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
if (session.config.target.overrides?.registrationEndpoint) {
|
|
442
|
+
session.context.authServerMetadata = {
|
|
443
|
+
...(session.context.authServerMetadata ?? {}),
|
|
444
|
+
registration_endpoint: session.config.target.overrides.registrationEndpoint
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
markStepCompleted(session, stepId, 'Metadata resolution finished');
|
|
448
|
+
}
|
|
449
|
+
async function stepResolveRegistrationSource(session) {
|
|
450
|
+
const stepId = 'resolve_registration_source';
|
|
451
|
+
if (!session.steps.some((s) => s.id === stepId))
|
|
452
|
+
return;
|
|
453
|
+
markStepStarted(session, stepId);
|
|
454
|
+
resolvedClientFromConfig(session);
|
|
455
|
+
if (session.context.resolvedClient?.clientId) {
|
|
456
|
+
markStepCompleted(session, stepId, `Client ${session.context.resolvedClient.clientId} resolved`);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
markStepCompleted(session, stepId, 'Registration source deferred');
|
|
460
|
+
}
|
|
461
|
+
async function stepFetchCimd(session) {
|
|
462
|
+
const stepId = 'fetch_cimd';
|
|
463
|
+
if (!session.steps.some((s) => s.id === stepId))
|
|
464
|
+
return;
|
|
465
|
+
markStepStarted(session, stepId);
|
|
466
|
+
const cimdUrl = session.config.clientConfig.cimd?.cimdUrl || session.config.target.overrides?.cimdUrl;
|
|
467
|
+
if (!cimdUrl)
|
|
468
|
+
throw new Error('CIMD URL is required for CIMD registration method');
|
|
469
|
+
const { response, responseJson, responseText } = await fetchWithTrace({
|
|
470
|
+
session,
|
|
471
|
+
stepId,
|
|
472
|
+
label: 'Client ID Metadata Document',
|
|
473
|
+
url: cimdUrl
|
|
474
|
+
});
|
|
475
|
+
if (!response.ok) {
|
|
476
|
+
throw new Error(`CIMD request failed (${response.status})`);
|
|
477
|
+
}
|
|
478
|
+
session.context.registration = responseJson ?? { raw: responseText };
|
|
479
|
+
const clientId = session.context.registration?.client_id;
|
|
480
|
+
if (!clientId) {
|
|
481
|
+
addValidation(session, {
|
|
482
|
+
stepId,
|
|
483
|
+
severity: 'error',
|
|
484
|
+
code: 'cimd_missing_client_id',
|
|
485
|
+
title: 'CIMD missing client_id',
|
|
486
|
+
detail: 'The Client ID Metadata Document does not contain a client_id.',
|
|
487
|
+
specReference: SPEC_BASE
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
resolvedClientFromConfig(session);
|
|
491
|
+
markStepCompleted(session, stepId, `Fetched CIMD${clientId ? ` for ${clientId}` : ''}`);
|
|
492
|
+
}
|
|
493
|
+
async function stepDcr(session) {
|
|
494
|
+
const stepId = 'dynamic_client_registration';
|
|
495
|
+
if (!session.steps.some((s) => s.id === stepId))
|
|
496
|
+
return;
|
|
497
|
+
markStepStarted(session, stepId);
|
|
498
|
+
const registrationEndpoint = session.context.authServerMetadata?.registration_endpoint;
|
|
499
|
+
if (!registrationEndpoint) {
|
|
500
|
+
throw new Error('Registration endpoint not available for DCR');
|
|
501
|
+
}
|
|
502
|
+
const redirectUri = requiredString(session.context.callbackUrl, 'Callback URL not set');
|
|
503
|
+
const bodyObj = {
|
|
504
|
+
redirect_uris: [redirectUri],
|
|
505
|
+
token_endpoint_auth_method: session.config.clientConfig.dcr?.tokenEndpointAuthMethod ?? 'none',
|
|
506
|
+
client_name: 'MCP Lab OAuth Debugger',
|
|
507
|
+
grant_types: ['authorization_code'],
|
|
508
|
+
response_types: ['code'],
|
|
509
|
+
...(session.config.clientConfig.dcr?.metadata ?? {})
|
|
510
|
+
};
|
|
511
|
+
const bodyText = JSON.stringify(bodyObj);
|
|
512
|
+
const { response, responseJson, responseText } = await fetchWithTrace({
|
|
513
|
+
session,
|
|
514
|
+
stepId,
|
|
515
|
+
label: 'Dynamic Client Registration',
|
|
516
|
+
url: String(registrationEndpoint),
|
|
517
|
+
method: 'POST',
|
|
518
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
519
|
+
bodyText
|
|
520
|
+
});
|
|
521
|
+
if (!response.ok) {
|
|
522
|
+
throw new Error(`DCR failed (${response.status})`);
|
|
523
|
+
}
|
|
524
|
+
session.context.registration = responseJson ?? { raw: responseText };
|
|
525
|
+
const clientId = session.context.registration?.client_id;
|
|
526
|
+
if (!clientId) {
|
|
527
|
+
throw new Error('DCR response missing client_id');
|
|
528
|
+
}
|
|
529
|
+
session.context.resolvedClient = {
|
|
530
|
+
clientId: String(clientId),
|
|
531
|
+
clientSecret: typeof session.context.registration?.client_secret === 'string'
|
|
532
|
+
? session.context.registration.client_secret
|
|
533
|
+
: undefined,
|
|
534
|
+
tokenEndpointAuthMethod: session.context.registration?.token_endpoint_auth_method ??
|
|
535
|
+
session.config.clientConfig.dcr?.tokenEndpointAuthMethod
|
|
536
|
+
};
|
|
537
|
+
markStepCompleted(session, stepId, `DCR created client ${clientId}`);
|
|
538
|
+
}
|
|
539
|
+
async function stepBuildAuthorizationRequest(session) {
|
|
540
|
+
const stepId = 'build_authorization_request';
|
|
541
|
+
markStepStarted(session, stepId);
|
|
542
|
+
const authUrl = buildAuthorizationUrl(session);
|
|
543
|
+
addValidation(session, {
|
|
544
|
+
stepId,
|
|
545
|
+
severity: 'info',
|
|
546
|
+
code: 'auth_url_built',
|
|
547
|
+
title: 'Authorization request constructed',
|
|
548
|
+
detail: 'Authorization request URL was built successfully.',
|
|
549
|
+
specReference: SPEC_BASE
|
|
550
|
+
});
|
|
551
|
+
markStepCompleted(session, stepId, authUrl);
|
|
552
|
+
}
|
|
553
|
+
async function stepBrowserAuthorizationPause(session) {
|
|
554
|
+
const stepId = 'browser_authorization';
|
|
555
|
+
markStepStarted(session, stepId);
|
|
556
|
+
const authUrl = session.context.authorizationRequestUrl;
|
|
557
|
+
if (!authUrl)
|
|
558
|
+
throw new Error('Authorization URL not built');
|
|
559
|
+
addSequence(session, {
|
|
560
|
+
from: 'User',
|
|
561
|
+
to: 'Auth Server',
|
|
562
|
+
label: 'Open authorization URL',
|
|
563
|
+
stepId
|
|
564
|
+
});
|
|
565
|
+
if (session.config.runtime.redirectMode === 'manual') {
|
|
566
|
+
session.status = 'waiting_for_user';
|
|
567
|
+
emitEvent(session, 'waiting_for_user', {
|
|
568
|
+
stepId,
|
|
569
|
+
nextAction: 'paste_callback_url',
|
|
570
|
+
authorizationUrl: authUrl
|
|
571
|
+
});
|
|
572
|
+
markStepCompleted(session, stepId, 'Waiting for manual callback paste');
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
session.status = 'waiting_for_browser_callback';
|
|
576
|
+
emitEvent(session, 'waiting_for_browser_callback', {
|
|
577
|
+
stepId,
|
|
578
|
+
nextAction: 'open_authorize_url',
|
|
579
|
+
authorizationUrl: authUrl,
|
|
580
|
+
callbackUrl: session.context.callbackUrl ?? null
|
|
581
|
+
});
|
|
582
|
+
markStepCompleted(session, stepId, 'Waiting for browser callback');
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function stepReceiveAuthorizationResponse(session) {
|
|
586
|
+
const stepId = 'receive_authorization_response';
|
|
587
|
+
markStepStarted(session, stepId);
|
|
588
|
+
if (!session.context.callbackResult) {
|
|
589
|
+
throw new Error('No authorization response callback captured');
|
|
590
|
+
}
|
|
591
|
+
markStepCompleted(session, stepId, 'Authorization response captured');
|
|
592
|
+
}
|
|
593
|
+
function stepValidateCallback(session) {
|
|
594
|
+
const stepId = 'validate_callback';
|
|
595
|
+
markStepStarted(session, stepId);
|
|
596
|
+
const cb = session.context.callbackResult;
|
|
597
|
+
if (!cb)
|
|
598
|
+
throw new Error('No callback result');
|
|
599
|
+
if (cb.error) {
|
|
600
|
+
addValidation(session, {
|
|
601
|
+
stepId,
|
|
602
|
+
severity: 'error',
|
|
603
|
+
code: 'authorization_error',
|
|
604
|
+
title: 'Authorization server returned an error',
|
|
605
|
+
detail: `${cb.error}${cb.errorDescription ? `: ${cb.errorDescription}` : ''}`,
|
|
606
|
+
recommendation: 'Inspect the authorization request parameters and client registration details.'
|
|
607
|
+
});
|
|
608
|
+
throw new Error(`Authorization error: ${cb.error}`);
|
|
609
|
+
}
|
|
610
|
+
if (!cb.code) {
|
|
611
|
+
addValidation(session, {
|
|
612
|
+
stepId,
|
|
613
|
+
severity: 'error',
|
|
614
|
+
code: 'missing_code',
|
|
615
|
+
title: 'Missing authorization code',
|
|
616
|
+
detail: 'The callback did not include an authorization code.',
|
|
617
|
+
specReference: SPEC_BASE
|
|
618
|
+
});
|
|
619
|
+
throw new Error('Authorization code missing from callback');
|
|
620
|
+
}
|
|
621
|
+
if (session.config.runtime.state && cb.state !== session.config.runtime.state) {
|
|
622
|
+
addValidation(session, {
|
|
623
|
+
stepId,
|
|
624
|
+
severity: 'error',
|
|
625
|
+
code: 'state_mismatch',
|
|
626
|
+
title: 'State mismatch',
|
|
627
|
+
detail: `Expected state '${session.config.runtime.state}' but received '${cb.state ?? ''}'.`,
|
|
628
|
+
recommendation: 'Verify redirect handling and ensure the authorization response belongs to this session.'
|
|
629
|
+
});
|
|
630
|
+
throw new Error('State mismatch');
|
|
631
|
+
}
|
|
632
|
+
addValidation(session, {
|
|
633
|
+
stepId,
|
|
634
|
+
severity: 'info',
|
|
635
|
+
code: 'callback_validated',
|
|
636
|
+
title: 'Authorization callback validated',
|
|
637
|
+
detail: 'Authorization code and state semantics look valid.',
|
|
638
|
+
specReference: SPEC_BASE
|
|
639
|
+
});
|
|
640
|
+
markStepCompleted(session, stepId, 'Callback validation passed');
|
|
641
|
+
}
|
|
642
|
+
async function stepTokenExchange(session) {
|
|
643
|
+
const stepId = 'token_exchange';
|
|
644
|
+
markStepStarted(session, stepId);
|
|
645
|
+
const tokenEndpoint = session.context.authServerMetadata?.token_endpoint;
|
|
646
|
+
if (!tokenEndpoint)
|
|
647
|
+
throw new Error('Token endpoint not resolved');
|
|
648
|
+
const client = session.context.resolvedClient;
|
|
649
|
+
if (!client?.clientId)
|
|
650
|
+
throw new Error('Client not resolved');
|
|
651
|
+
const cb = session.context.callbackResult;
|
|
652
|
+
if (!cb?.code)
|
|
653
|
+
throw new Error('Authorization code missing');
|
|
654
|
+
const redirectUri = requiredString(session.context.callbackUrl, 'Callback URL not set');
|
|
655
|
+
const form = new URLSearchParams({
|
|
656
|
+
grant_type: 'authorization_code',
|
|
657
|
+
code: cb.code,
|
|
658
|
+
redirect_uri: redirectUri,
|
|
659
|
+
client_id: client.clientId
|
|
660
|
+
});
|
|
661
|
+
if (session.config.runtime.usePkce && session.context.pkce?.verifier) {
|
|
662
|
+
form.set('code_verifier', session.context.pkce.verifier);
|
|
663
|
+
}
|
|
664
|
+
if (session.config.runtime.resource)
|
|
665
|
+
form.set('resource', session.config.runtime.resource);
|
|
666
|
+
const headers = {
|
|
667
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
668
|
+
accept: 'application/json'
|
|
669
|
+
};
|
|
670
|
+
if (client.clientSecret) {
|
|
671
|
+
const authMethod = client.tokenEndpointAuthMethod ?? 'client_secret_basic';
|
|
672
|
+
if (authMethod === 'client_secret_post') {
|
|
673
|
+
form.set('client_secret', client.clientSecret);
|
|
674
|
+
}
|
|
675
|
+
else {
|
|
676
|
+
headers.authorization = `Basic ${Buffer.from(`${client.clientId}:${client.clientSecret}`, 'utf8').toString('base64')}`;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
const { response, responseJson, responseText } = await fetchWithTrace({
|
|
680
|
+
session,
|
|
681
|
+
stepId,
|
|
682
|
+
label: 'Token request',
|
|
683
|
+
url: String(tokenEndpoint),
|
|
684
|
+
method: 'POST',
|
|
685
|
+
headers,
|
|
686
|
+
bodyText: form.toString()
|
|
687
|
+
});
|
|
688
|
+
session.context.tokenResponse = responseJson ?? { raw: responseText, status: response.status };
|
|
689
|
+
if (!response.ok) {
|
|
690
|
+
throw new Error(`Token exchange failed (${response.status})`);
|
|
691
|
+
}
|
|
692
|
+
markStepCompleted(session, stepId, `Token response HTTP ${response.status}`);
|
|
693
|
+
}
|
|
694
|
+
function stepTokenValidation(session) {
|
|
695
|
+
const stepId = 'token_validation';
|
|
696
|
+
markStepStarted(session, stepId);
|
|
697
|
+
const token = session.context.tokenResponse;
|
|
698
|
+
if (!token || typeof token !== 'object')
|
|
699
|
+
throw new Error('Token response missing');
|
|
700
|
+
if (!('access_token' in token)) {
|
|
701
|
+
addValidation(session, {
|
|
702
|
+
stepId,
|
|
703
|
+
severity: 'error',
|
|
704
|
+
code: 'token_missing_access_token',
|
|
705
|
+
title: 'Token response missing access_token',
|
|
706
|
+
detail: 'Token endpoint response did not include access_token.',
|
|
707
|
+
recommendation: 'Inspect token endpoint response and OAuth server configuration.'
|
|
708
|
+
});
|
|
709
|
+
throw new Error('Token response missing access_token');
|
|
710
|
+
}
|
|
711
|
+
if (!('token_type' in token)) {
|
|
712
|
+
addValidation(session, {
|
|
713
|
+
stepId,
|
|
714
|
+
severity: 'warning',
|
|
715
|
+
code: 'token_missing_token_type',
|
|
716
|
+
title: 'Token response missing token_type',
|
|
717
|
+
detail: 'Token response did not include token_type.',
|
|
718
|
+
recommendation: 'Most clients expect token_type (typically Bearer).'
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
addValidation(session, {
|
|
722
|
+
stepId,
|
|
723
|
+
severity: 'info',
|
|
724
|
+
code: 'token_response_validated',
|
|
725
|
+
title: 'Token response validated',
|
|
726
|
+
detail: 'Token response includes access_token and basic fields.',
|
|
727
|
+
specReference: SPEC_BASE
|
|
728
|
+
});
|
|
729
|
+
markStepCompleted(session, stepId, 'Token validation complete');
|
|
730
|
+
}
|
|
731
|
+
async function stepResourceProbe(session) {
|
|
732
|
+
const stepId = 'resource_probe';
|
|
733
|
+
markStepStarted(session, stepId);
|
|
734
|
+
const accessToken = typeof session.context.tokenResponse?.access_token === 'string'
|
|
735
|
+
? session.context.tokenResponse.access_token
|
|
736
|
+
: undefined;
|
|
737
|
+
const probeUrl = session.config.target.overrides?.resourceBaseUrl || session.serverConfig?.url;
|
|
738
|
+
if (!accessToken || !probeUrl) {
|
|
739
|
+
markStepSkipped(session, stepId, 'No access token or probe URL available');
|
|
740
|
+
emitEvent(session, 'log', {
|
|
741
|
+
message: 'Skipping protected probe (missing access token or probe URL)'
|
|
742
|
+
});
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
try {
|
|
746
|
+
const { response, responseText } = await fetchWithTrace({
|
|
747
|
+
session,
|
|
748
|
+
stepId,
|
|
749
|
+
label: 'Protected resource probe',
|
|
750
|
+
url: probeUrl,
|
|
751
|
+
headers: {
|
|
752
|
+
authorization: `Bearer ${accessToken}`,
|
|
753
|
+
accept: 'application/json'
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
session.context.probeResponse = {
|
|
757
|
+
status: response.status,
|
|
758
|
+
bodyText: responseText,
|
|
759
|
+
url: probeUrl
|
|
760
|
+
};
|
|
761
|
+
if (!response.ok) {
|
|
762
|
+
addValidation(session, {
|
|
763
|
+
stepId,
|
|
764
|
+
severity: 'warning',
|
|
765
|
+
code: 'probe_not_ok',
|
|
766
|
+
title: 'Protected probe returned non-success',
|
|
767
|
+
detail: `Protected probe returned HTTP ${response.status}.`,
|
|
768
|
+
recommendation: 'Verify audience/resource, scopes, and token issuer expectations on the MCP server.'
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
addValidation(session, {
|
|
773
|
+
stepId,
|
|
774
|
+
severity: 'info',
|
|
775
|
+
code: 'probe_ok',
|
|
776
|
+
title: 'Protected probe succeeded',
|
|
777
|
+
detail: 'The bearer token was accepted by the probe endpoint.'
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
markStepCompleted(session, stepId, `Probe HTTP ${response.status}`);
|
|
781
|
+
}
|
|
782
|
+
catch (error) {
|
|
783
|
+
addValidation(session, {
|
|
784
|
+
stepId,
|
|
785
|
+
severity: 'warning',
|
|
786
|
+
code: 'probe_failed',
|
|
787
|
+
title: 'Protected probe failed',
|
|
788
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
789
|
+
recommendation: 'If this endpoint is not a protected HTTP resource, ignore this warning or override resourceBaseUrl.'
|
|
790
|
+
});
|
|
791
|
+
markStepCompleted(session, stepId, 'Probe failed (captured as warning)');
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
function stepSummary(session) {
|
|
795
|
+
const stepId = 'summary';
|
|
796
|
+
markStepStarted(session, stepId);
|
|
797
|
+
const hasErrors = session.validations.some((v) => v.severity === 'error');
|
|
798
|
+
if (hasErrors) {
|
|
799
|
+
addValidation(session, {
|
|
800
|
+
stepId,
|
|
801
|
+
severity: 'info',
|
|
802
|
+
code: 'summary_failed',
|
|
803
|
+
title: 'OAuth debugger summary',
|
|
804
|
+
detail: 'One or more blocking validation errors were found.',
|
|
805
|
+
recommendation: 'Review failed steps and network exchanges to identify the protocol mismatch.'
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
addValidation(session, {
|
|
810
|
+
stepId,
|
|
811
|
+
severity: 'info',
|
|
812
|
+
code: 'summary_ok',
|
|
813
|
+
title: 'OAuth debugger summary',
|
|
814
|
+
detail: 'Flow completed without blocking validation errors.'
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
markStepCompleted(session, stepId, hasErrors ? 'Completed with validation errors' : 'Completed successfully');
|
|
818
|
+
}
|
|
819
|
+
function resetPendingStepStatesForResume(session) {
|
|
820
|
+
// no-op for now; step runner checks status
|
|
821
|
+
}
|
|
822
|
+
function nextPendingStep(session) {
|
|
823
|
+
return session.steps.find((s) => s.status === 'pending');
|
|
824
|
+
}
|
|
825
|
+
export function cleanupOAuthDebuggerSessions(sessions, now = Date.now()) {
|
|
826
|
+
for (const [id, session] of sessions) {
|
|
827
|
+
if (now - session.updatedAt > SESSION_TTL_MS) {
|
|
828
|
+
sessions.delete(id);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
export function createOAuthDebuggerSession(params) {
|
|
833
|
+
const runtime = normalizeRuntime(params.config.runtime);
|
|
834
|
+
const serverOauth = params.serverConfig?.auth?.type === 'oauth_authorization_code'
|
|
835
|
+
? params.serverConfig.auth
|
|
836
|
+
: undefined;
|
|
837
|
+
const clientConfig = params.config.registrationMethod === 'pre_registered'
|
|
838
|
+
? {
|
|
839
|
+
...params.config.clientConfig,
|
|
840
|
+
preRegistered: {
|
|
841
|
+
clientId: params.config.clientConfig.preRegistered?.clientId || serverOauth?.client_id || '',
|
|
842
|
+
clientSecret: params.config.clientConfig.preRegistered?.clientSecret ?? serverOauth?.client_secret,
|
|
843
|
+
tokenEndpointAuthMethod: params.config.clientConfig.preRegistered?.tokenEndpointAuthMethod
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
: params.config.clientConfig;
|
|
847
|
+
const session = {
|
|
848
|
+
id: makeId('oauthdbg'),
|
|
849
|
+
createdAt: Date.now(),
|
|
850
|
+
updatedAt: Date.now(),
|
|
851
|
+
status: 'configuring',
|
|
852
|
+
config: {
|
|
853
|
+
profile: 'latest',
|
|
854
|
+
target: params.config.target,
|
|
855
|
+
registrationMethod: params.config.registrationMethod,
|
|
856
|
+
clientConfig,
|
|
857
|
+
runtime: {
|
|
858
|
+
...runtime,
|
|
859
|
+
scopes: runtime.scopes && runtime.scopes.length > 0
|
|
860
|
+
? runtime.scopes
|
|
861
|
+
: serverOauth?.scope
|
|
862
|
+
? serverOauth.scope.split(/\s+/).filter(Boolean)
|
|
863
|
+
: []
|
|
864
|
+
},
|
|
865
|
+
display: {
|
|
866
|
+
showSensitiveValues: params.config.display?.showSensitiveValues !== false
|
|
867
|
+
}
|
|
868
|
+
},
|
|
869
|
+
steps: baseSteps(params.config.registrationMethod),
|
|
870
|
+
validations: [],
|
|
871
|
+
network: [],
|
|
872
|
+
sequence: [],
|
|
873
|
+
events: [],
|
|
874
|
+
clients: new Set(),
|
|
875
|
+
abortController: new AbortController(),
|
|
876
|
+
serverConfig: params.serverConfig,
|
|
877
|
+
context: {}
|
|
878
|
+
};
|
|
879
|
+
return session;
|
|
880
|
+
}
|
|
881
|
+
export function oauthDebuggerSessionView(session) {
|
|
882
|
+
const errorCount = session.network.filter((n) => n.phase === 'response' && typeof n.status === 'number' && n.status >= 400).length;
|
|
883
|
+
const token = session.context.tokenResponse && typeof session.context.tokenResponse === 'object'
|
|
884
|
+
? session.context.tokenResponse
|
|
885
|
+
: undefined;
|
|
886
|
+
return {
|
|
887
|
+
id: session.id,
|
|
888
|
+
status: session.status,
|
|
889
|
+
createdAt: new Date(session.createdAt).toISOString(),
|
|
890
|
+
updatedAt: new Date(session.updatedAt).toISOString(),
|
|
891
|
+
profile: session.config.profile,
|
|
892
|
+
registrationMethod: session.config.registrationMethod,
|
|
893
|
+
stepStates: session.steps,
|
|
894
|
+
validations: session.validations,
|
|
895
|
+
network: session.network,
|
|
896
|
+
networkSummary: {
|
|
897
|
+
requestCount: session.network.filter((n) => n.phase === 'request').length,
|
|
898
|
+
errorCount
|
|
899
|
+
},
|
|
900
|
+
sequence: session.sequence,
|
|
901
|
+
uiHints: {
|
|
902
|
+
nextAction: session.status === 'configuring'
|
|
903
|
+
? 'start'
|
|
904
|
+
: session.status === 'waiting_for_user'
|
|
905
|
+
? 'paste_callback_url'
|
|
906
|
+
: session.status === 'waiting_for_browser_callback'
|
|
907
|
+
? 'open_authorize_url'
|
|
908
|
+
: 'none',
|
|
909
|
+
authorizationUrl: session.context.authorizationRequestUrl,
|
|
910
|
+
callbackMode: session.config.runtime.redirectMode,
|
|
911
|
+
callbackUrl: session.context.callbackUrl
|
|
912
|
+
},
|
|
913
|
+
summary: {
|
|
914
|
+
issuer: session.context.authServerMetadata?.issuer ?? session.context.resourceMetadata?.issuer,
|
|
915
|
+
clientId: session.context.resolvedClient?.clientId,
|
|
916
|
+
redirectUri: session.context.callbackUrl,
|
|
917
|
+
tokenEndpointStatus: session.network
|
|
918
|
+
.filter((n) => n.label === 'Token request' && n.phase === 'response')
|
|
919
|
+
.slice(-1)[0]?.status,
|
|
920
|
+
tokenType: typeof token?.token_type === 'string' ? token.token_type : undefined,
|
|
921
|
+
grantedScopes: typeof token?.scope === 'string'
|
|
922
|
+
? String(token.scope).split(/\s+/).filter(Boolean)
|
|
923
|
+
: undefined
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
export async function startOrResumeOAuthDebuggerSession(params) {
|
|
928
|
+
const { session, appBaseUrl } = params;
|
|
929
|
+
if (session.status === 'stopped')
|
|
930
|
+
throw new Error('Session already stopped');
|
|
931
|
+
session.updatedAt = Date.now();
|
|
932
|
+
session.context.callbackUrl = localCallbackUrl(session, appBaseUrl);
|
|
933
|
+
if (session.status === 'configuring') {
|
|
934
|
+
emitEvent(session, 'started', {
|
|
935
|
+
sessionId: session.id,
|
|
936
|
+
registrationMethod: session.config.registrationMethod,
|
|
937
|
+
profile: session.config.profile
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
session.status = 'running';
|
|
941
|
+
resetPendingStepStatesForResume(session);
|
|
942
|
+
while (true) {
|
|
943
|
+
if (session.abortController.signal.aborted) {
|
|
944
|
+
session.status = 'stopped';
|
|
945
|
+
emitEvent(session, 'stopped', { message: 'OAuth debug session stopped by user' });
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
const pending = nextPendingStep(session);
|
|
949
|
+
if (!pending) {
|
|
950
|
+
session.status = 'completed';
|
|
951
|
+
emitEvent(session, 'completed', {
|
|
952
|
+
summary: oauthDebuggerSessionView(session).summary ?? null
|
|
953
|
+
});
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
try {
|
|
957
|
+
switch (pending.id) {
|
|
958
|
+
case 'resolve_target_metadata':
|
|
959
|
+
await stepResolveTargetMetadata(session);
|
|
960
|
+
break;
|
|
961
|
+
case 'resolve_registration_source':
|
|
962
|
+
await stepResolveRegistrationSource(session);
|
|
963
|
+
break;
|
|
964
|
+
case 'fetch_cimd':
|
|
965
|
+
await stepFetchCimd(session);
|
|
966
|
+
break;
|
|
967
|
+
case 'dynamic_client_registration':
|
|
968
|
+
await stepDcr(session);
|
|
969
|
+
break;
|
|
970
|
+
case 'build_authorization_request':
|
|
971
|
+
await stepBuildAuthorizationRequest(session);
|
|
972
|
+
break;
|
|
973
|
+
case 'browser_authorization':
|
|
974
|
+
await stepBrowserAuthorizationPause(session);
|
|
975
|
+
return;
|
|
976
|
+
case 'receive_authorization_response':
|
|
977
|
+
if (!session.context.callbackResult) {
|
|
978
|
+
session.status =
|
|
979
|
+
session.config.runtime.redirectMode === 'manual'
|
|
980
|
+
? 'waiting_for_user'
|
|
981
|
+
: 'waiting_for_browser_callback';
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
stepReceiveAuthorizationResponse(session);
|
|
985
|
+
break;
|
|
986
|
+
case 'validate_callback':
|
|
987
|
+
stepValidateCallback(session);
|
|
988
|
+
break;
|
|
989
|
+
case 'token_exchange':
|
|
990
|
+
await stepTokenExchange(session);
|
|
991
|
+
break;
|
|
992
|
+
case 'token_validation':
|
|
993
|
+
stepTokenValidation(session);
|
|
994
|
+
break;
|
|
995
|
+
case 'resource_probe':
|
|
996
|
+
await stepResourceProbe(session);
|
|
997
|
+
break;
|
|
998
|
+
case 'summary':
|
|
999
|
+
stepSummary(session);
|
|
1000
|
+
break;
|
|
1001
|
+
default:
|
|
1002
|
+
markStepSkipped(session, pending.id, 'Unsupported step in v1');
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
catch (error) {
|
|
1006
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1007
|
+
markStepFailed(session, pending.id, message);
|
|
1008
|
+
session.status = 'error';
|
|
1009
|
+
emitEvent(session, 'error', { message });
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
export function submitManualCallbackToSession(params) {
|
|
1015
|
+
const parsed = parseCallbackInput({
|
|
1016
|
+
redirectUrl: params.redirectUrl,
|
|
1017
|
+
code: params.code,
|
|
1018
|
+
state: params.state
|
|
1019
|
+
});
|
|
1020
|
+
params.session.context.callbackResult = parsed;
|
|
1021
|
+
params.session.updatedAt = Date.now();
|
|
1022
|
+
emitEvent(params.session, 'log', {
|
|
1023
|
+
message: 'Manual callback input received.'
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
export function submitBrowserCallbackToSession(params) {
|
|
1027
|
+
params.session.context.callbackResult = parseCallbackInput({ redirectUrl: params.rawUrl });
|
|
1028
|
+
params.session.updatedAt = Date.now();
|
|
1029
|
+
emitEvent(params.session, 'log', { message: 'Browser callback captured.' });
|
|
1030
|
+
}
|
|
1031
|
+
export function stopOAuthDebuggerSession(session) {
|
|
1032
|
+
if (session.status === 'running' ||
|
|
1033
|
+
session.status === 'waiting_for_user' ||
|
|
1034
|
+
session.status === 'waiting_for_browser_callback') {
|
|
1035
|
+
session.abortController.abort();
|
|
1036
|
+
session.status = 'stopped';
|
|
1037
|
+
session.updatedAt = Date.now();
|
|
1038
|
+
emitEvent(session, 'stopped', { message: 'Stop requested by user' });
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
export function oauthDebuggerExportMarkdown(session) {
|
|
1042
|
+
const view = oauthDebuggerSessionView(session);
|
|
1043
|
+
const lines = [];
|
|
1044
|
+
lines.push('# OAuth Debugger Report');
|
|
1045
|
+
lines.push('');
|
|
1046
|
+
lines.push(`- Session ID: ${view.id}`);
|
|
1047
|
+
lines.push(`- Status: ${view.status}`);
|
|
1048
|
+
lines.push(`- Profile: ${view.profile}`);
|
|
1049
|
+
lines.push(`- Registration method: ${view.registrationMethod}`);
|
|
1050
|
+
lines.push(`- Target MCP server: ${session.config.target.serverName}`);
|
|
1051
|
+
lines.push('');
|
|
1052
|
+
lines.push('## Steps');
|
|
1053
|
+
for (const s of view.stepStates) {
|
|
1054
|
+
lines.push(`- ${s.title}: ${s.status}${s.outcomeSummary ? ` — ${s.outcomeSummary}` : ''}`);
|
|
1055
|
+
}
|
|
1056
|
+
lines.push('');
|
|
1057
|
+
lines.push('## Findings');
|
|
1058
|
+
if (view.validations.length === 0) {
|
|
1059
|
+
lines.push('- No validation findings recorded.');
|
|
1060
|
+
}
|
|
1061
|
+
else {
|
|
1062
|
+
for (const v of view.validations) {
|
|
1063
|
+
lines.push(`- [${v.severity}] (${v.stepId}) ${v.title}: ${v.detail}`);
|
|
1064
|
+
if (v.recommendation)
|
|
1065
|
+
lines.push(` - Recommendation: ${v.recommendation}`);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
lines.push('');
|
|
1069
|
+
lines.push('## Network');
|
|
1070
|
+
for (const n of view.network.filter((e) => e.phase === 'response')) {
|
|
1071
|
+
lines.push(`- ${n.label}: ${n.status ?? '-'} ${n.url}`);
|
|
1072
|
+
}
|
|
1073
|
+
return `${lines.join('\n')}\n`;
|
|
1074
|
+
}
|
|
1075
|
+
export function oauthDebuggerExportRawTrace(session) {
|
|
1076
|
+
const lines = [];
|
|
1077
|
+
for (const ex of session.network) {
|
|
1078
|
+
if (ex.phase === 'request') {
|
|
1079
|
+
lines.push(`> ${ex.label}`);
|
|
1080
|
+
lines.push(`> ${ex.method ?? 'GET'} ${ex.url}`);
|
|
1081
|
+
for (const [k, v] of Object.entries(ex.headers))
|
|
1082
|
+
lines.push(`> ${k}: ${v}`);
|
|
1083
|
+
if (ex.bodyText)
|
|
1084
|
+
lines.push(`>\n${ex.bodyText}`);
|
|
1085
|
+
}
|
|
1086
|
+
else {
|
|
1087
|
+
lines.push(`< ${ex.label}`);
|
|
1088
|
+
lines.push(`< ${ex.status ?? '-'} ${ex.url}`);
|
|
1089
|
+
for (const [k, v] of Object.entries(ex.headers))
|
|
1090
|
+
lines.push(`< ${k}: ${v}`);
|
|
1091
|
+
if (ex.bodyText)
|
|
1092
|
+
lines.push(`<\n${ex.bodyText}`);
|
|
1093
|
+
}
|
|
1094
|
+
lines.push('');
|
|
1095
|
+
}
|
|
1096
|
+
return `${lines.join('\n')}\n`;
|
|
1097
|
+
}
|
|
1098
|
+
//# sourceMappingURL=oauth-debugger-domain.js.map
|