@gleanwork/mcp-server-tester 0.12.0 → 1.0.0-beta.1
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 +120 -337
- package/dist/cli/index.js +468 -176
- package/dist/fixtures/mcp.d.ts +121 -44
- package/dist/fixtures/mcp.js +988 -248
- package/dist/fixtures/mcp.js.map +1 -1
- package/dist/fixtures/mcpAuth.js +6 -2
- package/dist/fixtures/mcpAuth.js.map +1 -1
- package/dist/index.cjs +5034 -1284
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1697 -575
- package/dist/index.d.ts +1697 -575
- package/dist/index.js +5020 -1280
- package/dist/index.js.map +1 -1
- package/dist/reporters/mcpReporter.cjs +35 -16
- package/dist/reporters/mcpReporter.cjs.map +1 -1
- package/dist/reporters/mcpReporter.d.cts +8 -3
- package/dist/reporters/mcpReporter.d.ts +8 -3
- package/dist/reporters/mcpReporter.js +36 -17
- package/dist/reporters/mcpReporter.js.map +1 -1
- package/dist/reporters/ui-dist/app.js +5 -5
- package/dist/reporters/ui-dist/styles.css +1 -1
- package/package.json +64 -8
- package/src/reporters/ui-dist/app.js +5 -5
- package/src/reporters/ui-dist/styles.css +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -13,11 +13,14 @@ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
|
13
13
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
14
14
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
15
15
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
16
|
+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
16
17
|
import { z } from 'zod';
|
|
17
18
|
import createDebug from 'debug';
|
|
19
|
+
import { ProxyAgent, Agent } from 'undici';
|
|
20
|
+
import { readFileSync } from 'fs';
|
|
21
|
+
import * as oauth from 'oauth4webapi';
|
|
18
22
|
import { homedir } from 'os';
|
|
19
23
|
import * as http from 'http';
|
|
20
|
-
import * as oauth from 'oauth4webapi';
|
|
21
24
|
|
|
22
25
|
function Spinner({ label }) {
|
|
23
26
|
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
@@ -75,6 +78,10 @@ function JsonPreview({ data, maxLines = 15 }) {
|
|
|
75
78
|
);
|
|
76
79
|
}
|
|
77
80
|
|
|
81
|
+
// package.json
|
|
82
|
+
var package_default = {
|
|
83
|
+
version: "1.0.0-beta.1"};
|
|
84
|
+
|
|
78
85
|
// src/cli/templates/index.ts
|
|
79
86
|
function getPlaywrightConfigTemplate(answers) {
|
|
80
87
|
const mcpConfig = answers.transport === "stdio" ? `{
|
|
@@ -110,7 +117,6 @@ export default defineConfig({
|
|
|
110
117
|
['html'],
|
|
111
118
|
['@gleanwork/mcp-server-tester/reporters/mcpReporter', {
|
|
112
119
|
outputDir: '.mcp-test-results',
|
|
113
|
-
autoOpen: true,
|
|
114
120
|
historyLimit: 10
|
|
115
121
|
}]
|
|
116
122
|
],
|
|
@@ -251,7 +257,7 @@ function getPackageJsonTemplate(projectName) {
|
|
|
251
257
|
"dependencies": {
|
|
252
258
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
253
259
|
"@playwright/test": "^1.49.0",
|
|
254
|
-
"@gleanwork/mcp-server-tester": "
|
|
260
|
+
"@gleanwork/mcp-server-tester": "^${package_default.version}",
|
|
255
261
|
"zod": "^3.24.1"
|
|
256
262
|
},
|
|
257
263
|
"devDependencies": {
|
|
@@ -539,9 +545,16 @@ var MCPOAuthConfigSchema = z.object({
|
|
|
539
545
|
clientSecret: z.string().optional(),
|
|
540
546
|
redirectUri: z.string().url().optional()
|
|
541
547
|
});
|
|
548
|
+
var MCPClientCredentialsConfigSchema = z.object({
|
|
549
|
+
clientId: z.string().optional(),
|
|
550
|
+
clientSecret: z.string().optional(),
|
|
551
|
+
tokenEndpoint: z.string().url("tokenEndpoint must be a valid URL").optional(),
|
|
552
|
+
scopes: z.array(z.string()).optional()
|
|
553
|
+
});
|
|
542
554
|
var MCPAuthConfigSchema = z.object({
|
|
543
555
|
accessToken: z.string().optional(),
|
|
544
|
-
oauth: MCPOAuthConfigSchema.optional()
|
|
556
|
+
oauth: MCPOAuthConfigSchema.optional(),
|
|
557
|
+
clientCredentials: MCPClientCredentialsConfigSchema.optional()
|
|
545
558
|
}).refine(
|
|
546
559
|
(data) => !(data.accessToken && data.oauth),
|
|
547
560
|
"Cannot specify both accessToken and oauth configuration"
|
|
@@ -551,19 +564,48 @@ var StdioConfigSchema = z.object({
|
|
|
551
564
|
command: z.string().min(1, "command is required for stdio transport"),
|
|
552
565
|
args: z.array(z.string()).optional(),
|
|
553
566
|
cwd: z.string().optional(),
|
|
567
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
554
568
|
capabilities: MCPHostCapabilitiesSchema.optional(),
|
|
555
569
|
connectTimeoutMs: z.number().positive().optional(),
|
|
556
570
|
requestTimeoutMs: z.number().positive().optional(),
|
|
571
|
+
callTimeoutMs: z.number().positive().optional(),
|
|
557
572
|
quiet: z.boolean().optional()
|
|
558
573
|
});
|
|
574
|
+
function isLocalhost(hostname) {
|
|
575
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
576
|
+
}
|
|
559
577
|
var HttpConfigSchema = z.object({
|
|
560
578
|
transport: z.literal("http"),
|
|
561
|
-
serverUrl: z.string().url("serverUrl must be a valid URL")
|
|
579
|
+
serverUrl: z.string().url("serverUrl must be a valid URL").refine((url) => {
|
|
580
|
+
let parsed;
|
|
581
|
+
try {
|
|
582
|
+
parsed = new URL(url);
|
|
583
|
+
} catch {
|
|
584
|
+
return true;
|
|
585
|
+
}
|
|
586
|
+
if (parsed.protocol === "http:" && !isLocalhost(parsed.hostname)) {
|
|
587
|
+
console.warn(
|
|
588
|
+
`[mcp-server-tester] serverUrl uses http:// for non-localhost address "${parsed.hostname}". This transmits tokens unencrypted. Use https:// for remote servers.`
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
return true;
|
|
592
|
+
}),
|
|
562
593
|
headers: z.record(z.string()).optional(),
|
|
563
594
|
capabilities: MCPHostCapabilitiesSchema.optional(),
|
|
564
595
|
connectTimeoutMs: z.number().positive().optional(),
|
|
565
596
|
requestTimeoutMs: z.number().positive().optional(),
|
|
566
|
-
|
|
597
|
+
callTimeoutMs: z.number().positive().optional(),
|
|
598
|
+
auth: MCPAuthConfigSchema.optional(),
|
|
599
|
+
proxy: z.object({
|
|
600
|
+
url: z.string().url("proxy.url must be a valid URL")
|
|
601
|
+
}).optional(),
|
|
602
|
+
retryAttempts: z.number().int().min(0).optional(),
|
|
603
|
+
tls: z.object({
|
|
604
|
+
ca: z.string().optional(),
|
|
605
|
+
cert: z.string().optional(),
|
|
606
|
+
key: z.string().optional(),
|
|
607
|
+
rejectUnauthorized: z.boolean().optional()
|
|
608
|
+
}).optional()
|
|
567
609
|
});
|
|
568
610
|
var MCPConfigSchema = z.discriminatedUnion("transport", [
|
|
569
611
|
StdioConfigSchema,
|
|
@@ -573,26 +615,241 @@ function validateMCPConfig(config) {
|
|
|
573
615
|
return MCPConfigSchema.parse(config);
|
|
574
616
|
}
|
|
575
617
|
function isStdioConfig(config) {
|
|
576
|
-
return config.transport === "stdio"
|
|
618
|
+
return config.transport === "stdio";
|
|
577
619
|
}
|
|
578
620
|
function isHttpConfig(config) {
|
|
579
|
-
return config.transport === "http"
|
|
621
|
+
return config.transport === "http";
|
|
580
622
|
}
|
|
581
623
|
var NAMESPACE = "mcp-server-tester";
|
|
582
624
|
var debugClient = createDebug(`${NAMESPACE}:client`);
|
|
583
625
|
createDebug(`${NAMESPACE}:oauth`);
|
|
584
626
|
createDebug(`${NAMESPACE}:eval`);
|
|
627
|
+
var debugHttp = createDebug(`${NAMESPACE}:http`);
|
|
628
|
+
var debug = createDebug("mcp-server-tester:oauth-flow");
|
|
629
|
+
async function generatePKCE() {
|
|
630
|
+
const codeVerifier = oauth.generateRandomCodeVerifier();
|
|
631
|
+
const codeChallenge = await oauth.calculatePKCECodeChallenge(codeVerifier);
|
|
632
|
+
return {
|
|
633
|
+
codeVerifier,
|
|
634
|
+
codeChallenge
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function generateState() {
|
|
638
|
+
return oauth.generateRandomState();
|
|
639
|
+
}
|
|
640
|
+
function buildAuthorizationUrl(config) {
|
|
641
|
+
const authorizationEndpoint = config.authServer.server.authorization_endpoint;
|
|
642
|
+
if (!authorizationEndpoint) {
|
|
643
|
+
throw new Error(
|
|
644
|
+
"Authorization server does not have an authorization_endpoint"
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
const authorizationUrl = new URL(authorizationEndpoint);
|
|
648
|
+
authorizationUrl.searchParams.set("client_id", config.clientId);
|
|
649
|
+
authorizationUrl.searchParams.set("redirect_uri", config.redirectUri);
|
|
650
|
+
authorizationUrl.searchParams.set("response_type", "code");
|
|
651
|
+
authorizationUrl.searchParams.set("scope", config.scopes.join(" "));
|
|
652
|
+
authorizationUrl.searchParams.set("code_challenge", config.codeChallenge);
|
|
653
|
+
authorizationUrl.searchParams.set("code_challenge_method", "S256");
|
|
654
|
+
authorizationUrl.searchParams.set("state", config.state);
|
|
655
|
+
if (config.resource) {
|
|
656
|
+
authorizationUrl.searchParams.set("resource", config.resource);
|
|
657
|
+
}
|
|
658
|
+
return authorizationUrl;
|
|
659
|
+
}
|
|
660
|
+
async function exchangeCodeForTokens(config) {
|
|
661
|
+
const client = {
|
|
662
|
+
client_id: config.clientId,
|
|
663
|
+
token_endpoint_auth_method: config.clientSecret ? "client_secret_basic" : "none"
|
|
664
|
+
};
|
|
665
|
+
const clientAuth = config.clientSecret ? oauth.ClientSecretBasic(config.clientSecret) : oauth.None();
|
|
666
|
+
const callbackUrl = new URL(config.redirectUri);
|
|
667
|
+
callbackUrl.searchParams.set("code", config.code);
|
|
668
|
+
callbackUrl.searchParams.set("state", config.state);
|
|
669
|
+
const validatedParams = oauth.validateAuthResponse(
|
|
670
|
+
config.authServer.server,
|
|
671
|
+
client,
|
|
672
|
+
callbackUrl,
|
|
673
|
+
config.state
|
|
674
|
+
);
|
|
675
|
+
const response = await oauth.authorizationCodeGrantRequest(
|
|
676
|
+
config.authServer.server,
|
|
677
|
+
client,
|
|
678
|
+
clientAuth,
|
|
679
|
+
validatedParams,
|
|
680
|
+
config.redirectUri,
|
|
681
|
+
config.codeVerifier
|
|
682
|
+
);
|
|
683
|
+
const result = await oauth.processAuthorizationCodeResponse(
|
|
684
|
+
config.authServer.server,
|
|
685
|
+
client,
|
|
686
|
+
response
|
|
687
|
+
);
|
|
688
|
+
return {
|
|
689
|
+
accessToken: result.access_token,
|
|
690
|
+
tokenType: result.token_type,
|
|
691
|
+
expiresIn: result.expires_in,
|
|
692
|
+
refreshToken: result.refresh_token,
|
|
693
|
+
scope: result.scope
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
async function refreshAccessToken(config) {
|
|
697
|
+
const client = {
|
|
698
|
+
client_id: config.clientId,
|
|
699
|
+
token_endpoint_auth_method: config.clientSecret ? "client_secret_basic" : "none"
|
|
700
|
+
};
|
|
701
|
+
const clientAuth = config.clientSecret ? oauth.ClientSecretBasic(config.clientSecret) : oauth.None();
|
|
702
|
+
const response = await oauth.refreshTokenGrantRequest(
|
|
703
|
+
config.authServer.server,
|
|
704
|
+
client,
|
|
705
|
+
clientAuth,
|
|
706
|
+
config.refreshToken
|
|
707
|
+
);
|
|
708
|
+
if (!response.ok) {
|
|
709
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
710
|
+
let errorMessage = `Token refresh failed: ${response.status} ${response.statusText}`;
|
|
711
|
+
try {
|
|
712
|
+
if (contentType.includes("application/json")) {
|
|
713
|
+
const errorBody = await response.clone().json();
|
|
714
|
+
if (errorBody.error) {
|
|
715
|
+
errorMessage = `Token refresh failed: ${errorBody.error}`;
|
|
716
|
+
if (errorBody.error_description) {
|
|
717
|
+
errorMessage += ` - ${errorBody.error_description}`;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
} else {
|
|
721
|
+
const textBody = await response.clone().text();
|
|
722
|
+
if (textBody) {
|
|
723
|
+
errorMessage = `Token refresh failed: ${response.status} - ${textBody}`;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
} catch {
|
|
727
|
+
}
|
|
728
|
+
throw new Error(errorMessage);
|
|
729
|
+
}
|
|
730
|
+
const result = await oauth.processRefreshTokenResponse(
|
|
731
|
+
config.authServer.server,
|
|
732
|
+
client,
|
|
733
|
+
response
|
|
734
|
+
);
|
|
735
|
+
return {
|
|
736
|
+
accessToken: result.access_token,
|
|
737
|
+
tokenType: result.token_type,
|
|
738
|
+
expiresIn: result.expires_in,
|
|
739
|
+
refreshToken: result.refresh_token,
|
|
740
|
+
scope: result.scope
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
async function performClientCredentialsFlow(config) {
|
|
744
|
+
const tokenEndpointUrl = new URL(config.tokenEndpoint);
|
|
745
|
+
const authServer = {
|
|
746
|
+
issuer: tokenEndpointUrl.origin,
|
|
747
|
+
token_endpoint: config.tokenEndpoint
|
|
748
|
+
};
|
|
749
|
+
const client = {
|
|
750
|
+
client_id: config.clientId
|
|
751
|
+
};
|
|
752
|
+
const clientAuth = oauth.ClientSecretBasic(config.clientSecret);
|
|
753
|
+
const parameters = {};
|
|
754
|
+
if (config.scopes && config.scopes.length > 0) {
|
|
755
|
+
parameters["scope"] = config.scopes.join(" ");
|
|
756
|
+
}
|
|
757
|
+
const response = await oauth.clientCredentialsGrantRequest(
|
|
758
|
+
authServer,
|
|
759
|
+
client,
|
|
760
|
+
clientAuth,
|
|
761
|
+
parameters
|
|
762
|
+
);
|
|
763
|
+
const result = await oauth.processClientCredentialsResponse(
|
|
764
|
+
authServer,
|
|
765
|
+
client,
|
|
766
|
+
response
|
|
767
|
+
);
|
|
768
|
+
const requestedScopes = new Set(
|
|
769
|
+
config.scopes && config.scopes.length > 0 ? config.scopes : []
|
|
770
|
+
);
|
|
771
|
+
const grantedScopes = new Set(
|
|
772
|
+
(result.scope ?? "").split(" ").filter(Boolean)
|
|
773
|
+
);
|
|
774
|
+
const missingScopes = [...requestedScopes].filter(
|
|
775
|
+
(s) => !grantedScopes.has(s)
|
|
776
|
+
);
|
|
777
|
+
if (missingScopes.length > 0 && requestedScopes.size > 0 && grantedScopes.size > 0) {
|
|
778
|
+
debug(
|
|
779
|
+
"[oauth] Warning: Token server granted fewer scopes than requested. Missing: %s",
|
|
780
|
+
missingScopes.join(", ")
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
return {
|
|
784
|
+
accessToken: result.access_token,
|
|
785
|
+
tokenType: result.token_type,
|
|
786
|
+
expiresIn: result.expires_in,
|
|
787
|
+
scope: result.scope
|
|
788
|
+
};
|
|
789
|
+
}
|
|
585
790
|
|
|
586
791
|
// src/mcp/clientFactory.ts
|
|
792
|
+
function getRetryAfterDelayMs(err) {
|
|
793
|
+
const response = err?.response;
|
|
794
|
+
const retryAfter = response?.headers?.get?.("Retry-After");
|
|
795
|
+
if (retryAfter) {
|
|
796
|
+
const seconds = parseInt(retryAfter, 10);
|
|
797
|
+
if (!isNaN(seconds)) return seconds * 1e3;
|
|
798
|
+
}
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
function isRateLimitError(err) {
|
|
802
|
+
const response = err?.response;
|
|
803
|
+
return response?.status === 429;
|
|
804
|
+
}
|
|
805
|
+
function isTransientNetworkError(err) {
|
|
806
|
+
if (!(err instanceof Error)) return false;
|
|
807
|
+
const msg = err.message.toLowerCase();
|
|
808
|
+
return msg.includes("econnreset") || msg.includes("econnrefused") || msg.includes("etimedout") || msg.includes("enotfound") || msg.includes("network") || msg.includes("socket hang up") || msg.includes("fetch failed");
|
|
809
|
+
}
|
|
810
|
+
function isRetryableError(err) {
|
|
811
|
+
return isTransientNetworkError(err) || isRateLimitError(err);
|
|
812
|
+
}
|
|
813
|
+
async function retryWithBackoff(fn, maxAttempts) {
|
|
814
|
+
let lastErr;
|
|
815
|
+
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
|
|
816
|
+
try {
|
|
817
|
+
return await fn();
|
|
818
|
+
} catch (err) {
|
|
819
|
+
lastErr = err;
|
|
820
|
+
if (attempt < maxAttempts && isRetryableError(err)) {
|
|
821
|
+
const retryAfterMs = getRetryAfterDelayMs(err);
|
|
822
|
+
const delayMs = retryAfterMs !== null ? retryAfterMs : Math.min(1e3 * 2 ** attempt, 3e4);
|
|
823
|
+
debugClient(
|
|
824
|
+
"Retryable error on attempt %d/%d, retrying in %dms: %s",
|
|
825
|
+
attempt + 1,
|
|
826
|
+
maxAttempts + 1,
|
|
827
|
+
delayMs,
|
|
828
|
+
err.message
|
|
829
|
+
);
|
|
830
|
+
await new Promise((resolve3) => setTimeout(resolve3, delayMs));
|
|
831
|
+
} else {
|
|
832
|
+
throw err;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
throw lastErr;
|
|
837
|
+
}
|
|
838
|
+
var agentRegistry = /* @__PURE__ */ new WeakMap();
|
|
587
839
|
async function createMCPClientForConfig(config, options) {
|
|
588
840
|
const validatedConfig = validateMCPConfig(config);
|
|
589
841
|
const client = new Client(
|
|
590
842
|
{
|
|
591
843
|
name: "@gleanwork/mcp-server-tester",
|
|
592
|
-
version:
|
|
844
|
+
version: package_default.version
|
|
593
845
|
},
|
|
594
846
|
{
|
|
595
|
-
capabilities:
|
|
847
|
+
capabilities: {
|
|
848
|
+
...validatedConfig.capabilities ?? {},
|
|
849
|
+
// Only advertise sampling if a handler has been registered;
|
|
850
|
+
// declaring sampling capability without a handler violates the MCP spec
|
|
851
|
+
sampling: void 0
|
|
852
|
+
}
|
|
596
853
|
}
|
|
597
854
|
);
|
|
598
855
|
if (isStdioConfig(validatedConfig)) {
|
|
@@ -601,33 +858,140 @@ async function createMCPClientForConfig(config, options) {
|
|
|
601
858
|
args: validatedConfig.args ?? [],
|
|
602
859
|
...validatedConfig.cwd && { cwd: validatedConfig.cwd },
|
|
603
860
|
// Suppress server stderr when quiet mode is enabled
|
|
604
|
-
...validatedConfig.quiet && { stderr: "ignore" }
|
|
861
|
+
...validatedConfig.quiet && { stderr: "ignore" },
|
|
862
|
+
...validatedConfig.env && {
|
|
863
|
+
env: Object.fromEntries(
|
|
864
|
+
Object.entries({ ...process.env, ...validatedConfig.env }).filter(
|
|
865
|
+
(entry) => entry[1] !== void 0
|
|
866
|
+
)
|
|
867
|
+
)
|
|
868
|
+
}
|
|
605
869
|
});
|
|
606
870
|
debugClient("Connecting via stdio: %O", {
|
|
607
871
|
command: validatedConfig.command,
|
|
608
872
|
args: validatedConfig.args,
|
|
609
873
|
cwd: validatedConfig.cwd
|
|
610
874
|
});
|
|
611
|
-
await client.connect(
|
|
875
|
+
await client.connect(
|
|
876
|
+
transport,
|
|
877
|
+
validatedConfig.connectTimeoutMs !== void 0 ? { timeout: validatedConfig.connectTimeoutMs } : void 0
|
|
878
|
+
);
|
|
612
879
|
} else if (isHttpConfig(validatedConfig)) {
|
|
613
880
|
const headers = { ...validatedConfig.headers };
|
|
881
|
+
if (validatedConfig.auth?.clientCredentials && true) {
|
|
882
|
+
const ccConfig = validatedConfig.auth.clientCredentials;
|
|
883
|
+
const clientId = ccConfig.clientId ?? process.env["MCP_CLIENT_ID"];
|
|
884
|
+
const clientSecret = ccConfig.clientSecret ?? process.env["MCP_CLIENT_SECRET"];
|
|
885
|
+
if (!clientId || !clientSecret) {
|
|
886
|
+
throw new Error(
|
|
887
|
+
"Client credentials require clientId/clientSecret in config or MCP_CLIENT_ID/MCP_CLIENT_SECRET env vars"
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
if (!ccConfig.tokenEndpoint) {
|
|
891
|
+
throw new Error(
|
|
892
|
+
"Client credentials require tokenEndpoint in auth.clientCredentials config"
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
debugClient("Fetching token via client credentials grant");
|
|
896
|
+
const tokenResult = await performClientCredentialsFlow({
|
|
897
|
+
tokenEndpoint: ccConfig.tokenEndpoint,
|
|
898
|
+
clientId,
|
|
899
|
+
clientSecret,
|
|
900
|
+
scopes: ccConfig.scopes
|
|
901
|
+
});
|
|
902
|
+
headers.Authorization = `Bearer ${tokenResult.accessToken}`;
|
|
903
|
+
}
|
|
614
904
|
if (validatedConfig.auth?.accessToken && true) {
|
|
615
905
|
headers.Authorization = `Bearer ${validatedConfig.auth.accessToken}`;
|
|
616
906
|
}
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
907
|
+
const url = new URL(validatedConfig.serverUrl);
|
|
908
|
+
let requestInit = Object.keys(headers).length > 0 ? { headers } : void 0;
|
|
909
|
+
const proxyUrl = validatedConfig.proxy?.url ?? process.env["HTTPS_PROXY"] ?? process.env["HTTP_PROXY"];
|
|
910
|
+
if (proxyUrl) {
|
|
911
|
+
const proxyAgent = new ProxyAgent(proxyUrl);
|
|
912
|
+
try {
|
|
913
|
+
const sanitized = new URL(proxyUrl);
|
|
914
|
+
debugClient(
|
|
915
|
+
"Using proxy: %s://%s:%s",
|
|
916
|
+
sanitized.protocol.slice(0, -1),
|
|
917
|
+
sanitized.hostname,
|
|
918
|
+
sanitized.port
|
|
919
|
+
);
|
|
920
|
+
} catch {
|
|
921
|
+
debugClient("Using proxy (unparseable URL)");
|
|
922
|
+
}
|
|
923
|
+
requestInit = {
|
|
924
|
+
...requestInit,
|
|
925
|
+
dispatcher: proxyAgent
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
if (validatedConfig.tls) {
|
|
929
|
+
const tlsCfg = validatedConfig.tls;
|
|
930
|
+
try {
|
|
931
|
+
const dispatcher = new Agent({
|
|
932
|
+
connect: {
|
|
933
|
+
...tlsCfg.ca && { ca: readFileSync(tlsCfg.ca) },
|
|
934
|
+
...tlsCfg.cert && { cert: readFileSync(tlsCfg.cert) },
|
|
935
|
+
...tlsCfg.key && { key: readFileSync(tlsCfg.key) },
|
|
936
|
+
rejectUnauthorized: tlsCfg.rejectUnauthorized ?? true
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
agentRegistry.set(client, dispatcher);
|
|
940
|
+
requestInit = {
|
|
941
|
+
...requestInit,
|
|
942
|
+
dispatcher
|
|
943
|
+
};
|
|
944
|
+
debugClient("TLS configuration applied");
|
|
945
|
+
} catch (error) {
|
|
946
|
+
const filePath = tlsCfg.ca ?? tlsCfg.cert ?? tlsCfg.key;
|
|
947
|
+
const fileType = tlsCfg.ca ? "CA certificate" : tlsCfg.cert ? "client certificate" : "client key";
|
|
948
|
+
throw new Error(
|
|
949
|
+
`Failed to load TLS ${fileType} from ${filePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
950
|
+
);
|
|
623
951
|
}
|
|
624
|
-
)
|
|
952
|
+
} else if (proxyUrl) {
|
|
953
|
+
const existingDispatcher = requestInit?.dispatcher;
|
|
954
|
+
if (existingDispatcher) {
|
|
955
|
+
agentRegistry.set(client, existingDispatcher);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
625
958
|
debugClient("Connecting via HTTP: %O", {
|
|
626
959
|
serverUrl: validatedConfig.serverUrl,
|
|
627
960
|
headers: Object.keys(headers).length > 0 ? Object.keys(headers) : void 0,
|
|
628
961
|
hasAuthProvider: false
|
|
629
962
|
});
|
|
630
|
-
|
|
963
|
+
debugHttp("Connecting to %s", validatedConfig.serverUrl);
|
|
964
|
+
if (Object.keys(headers).length > 0) {
|
|
965
|
+
debugHttp("Request header names: %O", Object.keys(headers));
|
|
966
|
+
}
|
|
967
|
+
const retryAttempts = validatedConfig.retryAttempts ?? 0;
|
|
968
|
+
const connectOptions = validatedConfig.connectTimeoutMs !== void 0 ? { timeout: validatedConfig.connectTimeoutMs } : void 0;
|
|
969
|
+
await retryWithBackoff(async () => {
|
|
970
|
+
try {
|
|
971
|
+
debugHttp("Attempting transport: streamableHttp");
|
|
972
|
+
const streamableTransport = new StreamableHTTPClientTransport(url, {
|
|
973
|
+
requestInit,
|
|
974
|
+
authProvider: options?.authProvider
|
|
975
|
+
});
|
|
976
|
+
await client.connect(streamableTransport, connectOptions);
|
|
977
|
+
debugClient("Connected via Streamable HTTP");
|
|
978
|
+
debugHttp("Connection established via streamableHttp");
|
|
979
|
+
} catch (err) {
|
|
980
|
+
debugHttp(
|
|
981
|
+
"streamableHttp failed (%s), falling back to SSE",
|
|
982
|
+
err.message
|
|
983
|
+
);
|
|
984
|
+
debugClient("Streamable HTTP failed, falling back to SSE transport");
|
|
985
|
+
debugHttp("Attempting transport: sse");
|
|
986
|
+
const sseTransport = new SSEClientTransport(url, {
|
|
987
|
+
requestInit,
|
|
988
|
+
authProvider: options?.authProvider
|
|
989
|
+
});
|
|
990
|
+
await client.connect(sseTransport, connectOptions);
|
|
991
|
+
debugClient("Connected via SSE");
|
|
992
|
+
debugHttp("Connection established via sse");
|
|
993
|
+
}
|
|
994
|
+
}, retryAttempts);
|
|
631
995
|
}
|
|
632
996
|
debugClient("Connected successfully");
|
|
633
997
|
const serverInfo = client.getServerVersion();
|
|
@@ -640,8 +1004,24 @@ async function closeMCPClient(client) {
|
|
|
640
1004
|
try {
|
|
641
1005
|
await client.close();
|
|
642
1006
|
} catch (error) {
|
|
643
|
-
|
|
1007
|
+
debugClient(
|
|
1008
|
+
"Error closing client: %s",
|
|
1009
|
+
error instanceof Error ? error.message : String(error)
|
|
1010
|
+
);
|
|
644
1011
|
throw error;
|
|
1012
|
+
} finally {
|
|
1013
|
+
const agent = agentRegistry.get(client);
|
|
1014
|
+
if (agent) {
|
|
1015
|
+
agentRegistry.delete(client);
|
|
1016
|
+
try {
|
|
1017
|
+
await agent.close();
|
|
1018
|
+
} catch (agentError) {
|
|
1019
|
+
debugClient(
|
|
1020
|
+
"Error closing undici agent: %s",
|
|
1021
|
+
agentError.message
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
645
1025
|
}
|
|
646
1026
|
}
|
|
647
1027
|
var ENV_VAR_NAMES = {
|
|
@@ -820,119 +1200,27 @@ var FileOAuthStorage = class {
|
|
|
820
1200
|
}
|
|
821
1201
|
}
|
|
822
1202
|
};
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
}
|
|
831
|
-
function generateState() {
|
|
832
|
-
return oauth.generateRandomState();
|
|
833
|
-
}
|
|
834
|
-
function buildAuthorizationUrl(config) {
|
|
835
|
-
const authorizationEndpoint = config.authServer.server.authorization_endpoint;
|
|
836
|
-
if (!authorizationEndpoint) {
|
|
837
|
-
throw new Error(
|
|
838
|
-
"Authorization server does not have an authorization_endpoint"
|
|
839
|
-
);
|
|
840
|
-
}
|
|
841
|
-
const authorizationUrl = new URL(authorizationEndpoint);
|
|
842
|
-
authorizationUrl.searchParams.set("client_id", config.clientId);
|
|
843
|
-
authorizationUrl.searchParams.set("redirect_uri", config.redirectUri);
|
|
844
|
-
authorizationUrl.searchParams.set("response_type", "code");
|
|
845
|
-
authorizationUrl.searchParams.set("scope", config.scopes.join(" "));
|
|
846
|
-
authorizationUrl.searchParams.set("code_challenge", config.codeChallenge);
|
|
847
|
-
authorizationUrl.searchParams.set("code_challenge_method", "S256");
|
|
848
|
-
authorizationUrl.searchParams.set("state", config.state);
|
|
849
|
-
if (config.resource) {
|
|
850
|
-
authorizationUrl.searchParams.set("resource", config.resource);
|
|
1203
|
+
function isLocalhostUrl(url) {
|
|
1204
|
+
try {
|
|
1205
|
+
const parsed = new URL(url);
|
|
1206
|
+
const h = parsed.hostname;
|
|
1207
|
+
return h === "localhost" || h === "127.0.0.1" || h === "::1";
|
|
1208
|
+
} catch {
|
|
1209
|
+
return false;
|
|
851
1210
|
}
|
|
852
|
-
return authorizationUrl;
|
|
853
|
-
}
|
|
854
|
-
async function exchangeCodeForTokens(config) {
|
|
855
|
-
const client = {
|
|
856
|
-
client_id: config.clientId,
|
|
857
|
-
token_endpoint_auth_method: config.clientSecret ? "client_secret_basic" : "none"
|
|
858
|
-
};
|
|
859
|
-
const clientAuth = config.clientSecret ? oauth.ClientSecretBasic(config.clientSecret) : oauth.None();
|
|
860
|
-
const callbackUrl = new URL(config.redirectUri);
|
|
861
|
-
callbackUrl.searchParams.set("code", config.code);
|
|
862
|
-
callbackUrl.searchParams.set("state", config.state);
|
|
863
|
-
const validatedParams = oauth.validateAuthResponse(
|
|
864
|
-
config.authServer.server,
|
|
865
|
-
client,
|
|
866
|
-
callbackUrl,
|
|
867
|
-
config.state
|
|
868
|
-
);
|
|
869
|
-
const response = await oauth.authorizationCodeGrantRequest(
|
|
870
|
-
config.authServer.server,
|
|
871
|
-
client,
|
|
872
|
-
clientAuth,
|
|
873
|
-
validatedParams,
|
|
874
|
-
config.redirectUri,
|
|
875
|
-
config.codeVerifier
|
|
876
|
-
);
|
|
877
|
-
const result = await oauth.processAuthorizationCodeResponse(
|
|
878
|
-
config.authServer.server,
|
|
879
|
-
client,
|
|
880
|
-
response
|
|
881
|
-
);
|
|
882
|
-
return {
|
|
883
|
-
accessToken: result.access_token,
|
|
884
|
-
tokenType: result.token_type,
|
|
885
|
-
expiresIn: result.expires_in,
|
|
886
|
-
refreshToken: result.refresh_token,
|
|
887
|
-
scope: result.scope
|
|
888
|
-
};
|
|
889
1211
|
}
|
|
890
|
-
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
config.refreshToken
|
|
901
|
-
);
|
|
902
|
-
if (!response.ok) {
|
|
903
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
904
|
-
let errorMessage = `Token refresh failed: ${response.status} ${response.statusText}`;
|
|
905
|
-
try {
|
|
906
|
-
if (contentType.includes("application/json")) {
|
|
907
|
-
const errorBody = await response.clone().json();
|
|
908
|
-
if (errorBody.error) {
|
|
909
|
-
errorMessage = `Token refresh failed: ${errorBody.error}`;
|
|
910
|
-
if (errorBody.error_description) {
|
|
911
|
-
errorMessage += ` - ${errorBody.error_description}`;
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
} else {
|
|
915
|
-
const textBody = await response.clone().text();
|
|
916
|
-
if (textBody) {
|
|
917
|
-
errorMessage = `Token refresh failed: ${response.status} - ${textBody}`;
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
} catch {
|
|
1212
|
+
function validateAuthServerEndpoints(authServer) {
|
|
1213
|
+
const endpoints = [
|
|
1214
|
+
{ name: "authorization_endpoint", url: authServer.authorization_endpoint },
|
|
1215
|
+
{ name: "token_endpoint", url: authServer.token_endpoint }
|
|
1216
|
+
];
|
|
1217
|
+
for (const { name, url } of endpoints) {
|
|
1218
|
+
if (url && !url.startsWith("https://") && !isLocalhostUrl(url)) {
|
|
1219
|
+
throw new Error(
|
|
1220
|
+
`OAuth discovery returned an insecure ${name}: "${url}". Only HTTPS endpoints are permitted for OAuth flows to prevent token interception.`
|
|
1221
|
+
);
|
|
921
1222
|
}
|
|
922
|
-
throw new Error(errorMessage);
|
|
923
1223
|
}
|
|
924
|
-
const result = await oauth.processRefreshTokenResponse(
|
|
925
|
-
config.authServer.server,
|
|
926
|
-
client,
|
|
927
|
-
response
|
|
928
|
-
);
|
|
929
|
-
return {
|
|
930
|
-
accessToken: result.access_token,
|
|
931
|
-
tokenType: result.token_type,
|
|
932
|
-
expiresIn: result.expires_in,
|
|
933
|
-
refreshToken: result.refresh_token,
|
|
934
|
-
scope: result.scope
|
|
935
|
-
};
|
|
936
1224
|
}
|
|
937
1225
|
var MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
938
1226
|
async function discoverProtectedResource(mcpServerUrl) {
|
|
@@ -1002,6 +1290,7 @@ async function discoverAuthorizationServer(authServerUrl) {
|
|
|
1002
1290
|
})
|
|
1003
1291
|
});
|
|
1004
1292
|
const metadata = await oauth.processDiscoveryResponse(issuer, response);
|
|
1293
|
+
validateAuthServerEndpoints(metadata);
|
|
1005
1294
|
return {
|
|
1006
1295
|
server: metadata,
|
|
1007
1296
|
issuer: authServerUrl
|
|
@@ -1009,7 +1298,7 @@ async function discoverAuthorizationServer(authServerUrl) {
|
|
|
1009
1298
|
}
|
|
1010
1299
|
|
|
1011
1300
|
// src/auth/cli.ts
|
|
1012
|
-
var
|
|
1301
|
+
var debug2 = createDebug("mcp-server-tester:cli-oauth");
|
|
1013
1302
|
var DEFAULT_TIMEOUT_MS = 3e5;
|
|
1014
1303
|
var DEFAULT_CLIENT_NAME = "@gleanwork/mcp-server-tester";
|
|
1015
1304
|
var DEFAULT_METADATA_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -1035,7 +1324,7 @@ var CLIOAuthClient = class {
|
|
|
1035
1324
|
async getAccessToken() {
|
|
1036
1325
|
const envTokens = loadTokensFromEnv();
|
|
1037
1326
|
if (envTokens) {
|
|
1038
|
-
|
|
1327
|
+
debug2("Using tokens from environment variables");
|
|
1039
1328
|
return {
|
|
1040
1329
|
accessToken: envTokens.accessToken,
|
|
1041
1330
|
tokenType: envTokens.tokenType,
|
|
@@ -1048,7 +1337,7 @@ var CLIOAuthClient = class {
|
|
|
1048
1337
|
if (storedTokens?.accessToken) {
|
|
1049
1338
|
const isValid = await this.storage.hasValidToken();
|
|
1050
1339
|
if (isValid) {
|
|
1051
|
-
|
|
1340
|
+
debug2("Using cached tokens from storage");
|
|
1052
1341
|
return {
|
|
1053
1342
|
accessToken: storedTokens.accessToken,
|
|
1054
1343
|
tokenType: storedTokens.tokenType,
|
|
@@ -1058,7 +1347,7 @@ var CLIOAuthClient = class {
|
|
|
1058
1347
|
};
|
|
1059
1348
|
}
|
|
1060
1349
|
if (storedTokens.refreshToken) {
|
|
1061
|
-
|
|
1350
|
+
debug2("Token expired, attempting refresh");
|
|
1062
1351
|
try {
|
|
1063
1352
|
const refreshedTokens = await this.refreshStoredToken(storedTokens);
|
|
1064
1353
|
return {
|
|
@@ -1069,11 +1358,11 @@ var CLIOAuthClient = class {
|
|
|
1069
1358
|
fromEnv: false
|
|
1070
1359
|
};
|
|
1071
1360
|
} catch (error) {
|
|
1072
|
-
|
|
1361
|
+
debug2("Token refresh failed, will re-authenticate:", error);
|
|
1073
1362
|
}
|
|
1074
1363
|
}
|
|
1075
1364
|
}
|
|
1076
|
-
|
|
1365
|
+
debug2("Performing full OAuth authentication");
|
|
1077
1366
|
return this.authenticate();
|
|
1078
1367
|
}
|
|
1079
1368
|
/**
|
|
@@ -1089,7 +1378,7 @@ var CLIOAuthClient = class {
|
|
|
1089
1378
|
async tryGetAccessToken() {
|
|
1090
1379
|
const envTokens = loadTokensFromEnv();
|
|
1091
1380
|
if (envTokens) {
|
|
1092
|
-
|
|
1381
|
+
debug2("Using tokens from environment variables");
|
|
1093
1382
|
return {
|
|
1094
1383
|
accessToken: envTokens.accessToken,
|
|
1095
1384
|
tokenType: envTokens.tokenType,
|
|
@@ -1102,7 +1391,7 @@ var CLIOAuthClient = class {
|
|
|
1102
1391
|
if (storedTokens?.accessToken) {
|
|
1103
1392
|
const isValid = await this.storage.hasValidToken();
|
|
1104
1393
|
if (isValid) {
|
|
1105
|
-
|
|
1394
|
+
debug2("Using cached tokens from storage");
|
|
1106
1395
|
return {
|
|
1107
1396
|
accessToken: storedTokens.accessToken,
|
|
1108
1397
|
tokenType: storedTokens.tokenType,
|
|
@@ -1112,7 +1401,7 @@ var CLIOAuthClient = class {
|
|
|
1112
1401
|
};
|
|
1113
1402
|
}
|
|
1114
1403
|
if (storedTokens.refreshToken) {
|
|
1115
|
-
|
|
1404
|
+
debug2("Token expired, attempting refresh");
|
|
1116
1405
|
try {
|
|
1117
1406
|
const refreshedTokens = await this.refreshStoredToken(storedTokens);
|
|
1118
1407
|
return {
|
|
@@ -1123,12 +1412,12 @@ var CLIOAuthClient = class {
|
|
|
1123
1412
|
fromEnv: false
|
|
1124
1413
|
};
|
|
1125
1414
|
} catch (error) {
|
|
1126
|
-
|
|
1415
|
+
debug2("Token refresh failed:", error);
|
|
1127
1416
|
return null;
|
|
1128
1417
|
}
|
|
1129
1418
|
}
|
|
1130
1419
|
}
|
|
1131
|
-
|
|
1420
|
+
debug2("No valid token available");
|
|
1132
1421
|
return null;
|
|
1133
1422
|
}
|
|
1134
1423
|
/**
|
|
@@ -1163,7 +1452,7 @@ var CLIOAuthClient = class {
|
|
|
1163
1452
|
*/
|
|
1164
1453
|
async clearCredentials() {
|
|
1165
1454
|
await this.storage.deleteTokens();
|
|
1166
|
-
|
|
1455
|
+
debug2("Cleared stored credentials");
|
|
1167
1456
|
}
|
|
1168
1457
|
/**
|
|
1169
1458
|
* Discover protected resource and authorization server
|
|
@@ -1173,12 +1462,12 @@ var CLIOAuthClient = class {
|
|
|
1173
1462
|
if (cachedMetadata) {
|
|
1174
1463
|
const age = Date.now() - cachedMetadata.discoveredAt;
|
|
1175
1464
|
if (age < DEFAULT_METADATA_TTL_MS) {
|
|
1176
|
-
|
|
1177
|
-
|
|
1465
|
+
debug2("Using cached server metadata (age: %dms)", age);
|
|
1466
|
+
debug2(
|
|
1178
1467
|
"Cached protected resource scopes: %O",
|
|
1179
1468
|
cachedMetadata.protectedResource.scopes_supported
|
|
1180
1469
|
);
|
|
1181
|
-
|
|
1470
|
+
debug2(
|
|
1182
1471
|
"Cached auth server scopes: %O",
|
|
1183
1472
|
cachedMetadata.authServer.server.scopes_supported
|
|
1184
1473
|
);
|
|
@@ -1187,12 +1476,12 @@ var CLIOAuthClient = class {
|
|
|
1187
1476
|
authServer: cachedMetadata.authServer
|
|
1188
1477
|
};
|
|
1189
1478
|
}
|
|
1190
|
-
|
|
1479
|
+
debug2("Cached server metadata is stale (age: %dms), re-discovering", age);
|
|
1191
1480
|
}
|
|
1192
|
-
|
|
1481
|
+
debug2("Discovering protected resource:", this.config.mcpServerUrl);
|
|
1193
1482
|
const prResult = await discoverProtectedResource(this.config.mcpServerUrl);
|
|
1194
|
-
|
|
1195
|
-
|
|
1483
|
+
debug2("Found protected resource:", prResult.metadata.resource);
|
|
1484
|
+
debug2(
|
|
1196
1485
|
"Protected resource scopes_supported: %O",
|
|
1197
1486
|
prResult.metadata.scopes_supported
|
|
1198
1487
|
);
|
|
@@ -1202,10 +1491,10 @@ var CLIOAuthClient = class {
|
|
|
1202
1491
|
"No authorization servers found in protected resource metadata"
|
|
1203
1492
|
);
|
|
1204
1493
|
}
|
|
1205
|
-
|
|
1494
|
+
debug2("Discovering authorization server:", authServerUrl);
|
|
1206
1495
|
const authServer = await discoverAuthorizationServer(authServerUrl);
|
|
1207
|
-
|
|
1208
|
-
|
|
1496
|
+
debug2("Found authorization server:", authServer.issuer);
|
|
1497
|
+
debug2(
|
|
1209
1498
|
"Auth server scopes_supported: %O",
|
|
1210
1499
|
authServer.server.scopes_supported
|
|
1211
1500
|
);
|
|
@@ -1225,7 +1514,7 @@ var CLIOAuthClient = class {
|
|
|
1225
1514
|
*/
|
|
1226
1515
|
async getOrRegisterClient(authServer) {
|
|
1227
1516
|
if (this.config.clientId) {
|
|
1228
|
-
|
|
1517
|
+
debug2("Using pre-configured client ID");
|
|
1229
1518
|
return {
|
|
1230
1519
|
clientId: this.config.clientId,
|
|
1231
1520
|
clientSecret: this.config.clientSecret
|
|
@@ -1233,10 +1522,10 @@ var CLIOAuthClient = class {
|
|
|
1233
1522
|
}
|
|
1234
1523
|
const cachedClient = await this.storage.loadClient();
|
|
1235
1524
|
if (cachedClient?.clientId) {
|
|
1236
|
-
|
|
1525
|
+
debug2("Using cached client registration");
|
|
1237
1526
|
return cachedClient;
|
|
1238
1527
|
}
|
|
1239
|
-
|
|
1528
|
+
debug2("Registering new client via DCR");
|
|
1240
1529
|
const client = await this.registerClient(authServer);
|
|
1241
1530
|
await this.storage.saveClient(client);
|
|
1242
1531
|
return client;
|
|
@@ -1274,7 +1563,7 @@ ${errorText}`
|
|
|
1274
1563
|
);
|
|
1275
1564
|
}
|
|
1276
1565
|
const data = await response.json();
|
|
1277
|
-
|
|
1566
|
+
debug2("Client registered:", data.client_id);
|
|
1278
1567
|
return {
|
|
1279
1568
|
clientId: data.client_id,
|
|
1280
1569
|
clientSecret: data.client_secret,
|
|
@@ -1292,17 +1581,17 @@ ${errorText}`
|
|
|
1292
1581
|
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
1293
1582
|
try {
|
|
1294
1583
|
const requestedScopes = this.config.scopes ?? protectedResource.scopes_supported ?? authServer.server.scopes_supported ?? ["openid"];
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1584
|
+
debug2("Scope resolution:");
|
|
1585
|
+
debug2(" - User config scopes: %O", this.config.scopes);
|
|
1586
|
+
debug2(
|
|
1298
1587
|
" - Protected resource scopes_supported: %O",
|
|
1299
1588
|
protectedResource.scopes_supported
|
|
1300
1589
|
);
|
|
1301
|
-
|
|
1590
|
+
debug2(
|
|
1302
1591
|
" - Auth server scopes_supported: %O",
|
|
1303
1592
|
authServer.server.scopes_supported
|
|
1304
1593
|
);
|
|
1305
|
-
|
|
1594
|
+
debug2(" - Final requested scopes: %O", requestedScopes);
|
|
1306
1595
|
const authUrl = buildAuthorizationUrl({
|
|
1307
1596
|
authServer,
|
|
1308
1597
|
clientId: client.clientId,
|
|
@@ -1312,16 +1601,19 @@ ${errorText}`
|
|
|
1312
1601
|
state,
|
|
1313
1602
|
resource: protectedResource.resource
|
|
1314
1603
|
});
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1604
|
+
debug2(
|
|
1605
|
+
"Authorization URL (base): %s",
|
|
1606
|
+
`${authUrl.origin}${authUrl.pathname}`
|
|
1607
|
+
);
|
|
1608
|
+
debug2("Authorization URL params:");
|
|
1609
|
+
debug2(" - client_id: %s", authUrl.searchParams.get("client_id"));
|
|
1610
|
+
debug2(" - redirect_uri: %s", authUrl.searchParams.get("redirect_uri"));
|
|
1611
|
+
debug2(" - scope: %s", authUrl.searchParams.get("scope"));
|
|
1612
|
+
debug2(" - resource: %s", authUrl.searchParams.get("resource"));
|
|
1321
1613
|
await this.openBrowserOrPrintUrl(authUrl);
|
|
1322
|
-
|
|
1614
|
+
debug2("Waiting for OAuth callback...");
|
|
1323
1615
|
const code = await codePromise;
|
|
1324
|
-
|
|
1616
|
+
debug2("Received authorization code");
|
|
1325
1617
|
const tokenResult = await exchangeCodeForTokens({
|
|
1326
1618
|
authServer,
|
|
1327
1619
|
clientId: client.clientId,
|
|
@@ -1359,14 +1651,14 @@ ${errorText}`
|
|
|
1359
1651
|
let clientId;
|
|
1360
1652
|
let clientSecret;
|
|
1361
1653
|
if (storedTokens.clientId) {
|
|
1362
|
-
|
|
1654
|
+
debug2("Using clientId from stored tokens for refresh");
|
|
1363
1655
|
clientId = storedTokens.clientId;
|
|
1364
1656
|
const storedClient = await this.storage.loadClient();
|
|
1365
1657
|
if (storedClient?.clientId === clientId) {
|
|
1366
1658
|
clientSecret = storedClient.clientSecret;
|
|
1367
1659
|
}
|
|
1368
1660
|
} else {
|
|
1369
|
-
|
|
1661
|
+
debug2(
|
|
1370
1662
|
"No clientId in stored tokens, falling back to stored client (legacy behavior)"
|
|
1371
1663
|
);
|
|
1372
1664
|
const client = await this.getOrRegisterClient(metadata.authServer);
|
|
@@ -1460,7 +1752,7 @@ ${errorText}`
|
|
|
1460
1752
|
const preferredPort = this.config.callbackPort ?? 0;
|
|
1461
1753
|
server.listen(preferredPort, "127.0.0.1", () => {
|
|
1462
1754
|
const address = server.address();
|
|
1463
|
-
|
|
1755
|
+
debug2("Callback server listening on port", address.port);
|
|
1464
1756
|
resolve3({ port: address.port, codePromise, close: forceClose });
|
|
1465
1757
|
});
|
|
1466
1758
|
server.on("error", (err) => {
|
|
@@ -1484,9 +1776,9 @@ ${errorText}`
|
|
|
1484
1776
|
try {
|
|
1485
1777
|
const open = await import('open');
|
|
1486
1778
|
await open.default(url.toString());
|
|
1487
|
-
|
|
1779
|
+
debug2("Opened browser for authentication");
|
|
1488
1780
|
} catch (error) {
|
|
1489
|
-
|
|
1781
|
+
debug2("Failed to open browser:", error);
|
|
1490
1782
|
console.log("\nFailed to open browser automatically.");
|
|
1491
1783
|
console.log("Please open the following URL manually:\n");
|
|
1492
1784
|
console.log(url.toString() + "\n");
|
|
@@ -1697,7 +1989,7 @@ function isCallToolResult(value) {
|
|
|
1697
1989
|
return false;
|
|
1698
1990
|
}
|
|
1699
1991
|
const v = value;
|
|
1700
|
-
return Array.isArray(v.content)
|
|
1992
|
+
return Array.isArray(v.content);
|
|
1701
1993
|
}
|
|
1702
1994
|
function extractTextFromContentArray(content) {
|
|
1703
1995
|
const textParts = [];
|
|
@@ -2770,7 +3062,7 @@ async function token(serverUrl, options) {
|
|
|
2770
3062
|
|
|
2771
3063
|
// src/cli/index.ts
|
|
2772
3064
|
var program = new Command();
|
|
2773
|
-
program.name("mcp-server-tester").description("CLI tools for MCP server evaluation and testing").version(
|
|
3065
|
+
program.name("mcp-server-tester").description("CLI tools for MCP server evaluation and testing").version(package_default.version);
|
|
2774
3066
|
program.command("init").description("Initialize a new MCP evaluation project").option("-n, --name <name>", "Project name").option("-d, --dir <directory>", "Target directory", ".").action(init);
|
|
2775
3067
|
program.command("generate").alias("gen").description("Generate eval dataset by interacting with MCP server").option("-c, --config <path>", "Path to MCP config").option("-o, --output <path>", "Output dataset path", "data/dataset.json").option("-s, --snapshot", "Use Playwright snapshot testing for all cases").action(generate);
|
|
2776
3068
|
program.command("login").description("Authenticate with an MCP server via OAuth").argument("<server-url>", "MCP server URL to authenticate with").option("--force", "Force re-authentication even if valid token exists").option("--state-dir <dir>", "Custom directory for token storage").option(
|