@gleanwork/mcp-server-tester 0.12.0 → 1.0.0-beta.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 +120 -337
- package/dist/cli/index.js +455 -174
- package/dist/fixtures/mcp.d.ts +121 -44
- package/dist/fixtures/mcp.js +974 -244
- 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 +4936 -1292
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1660 -570
- package/dist/index.d.ts +1660 -570
- package/dist/index.js +4923 -1288
- 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 +63 -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.0"};
|
|
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"
|
|
@@ -554,16 +567,44 @@ var StdioConfigSchema = z.object({
|
|
|
554
567
|
capabilities: MCPHostCapabilitiesSchema.optional(),
|
|
555
568
|
connectTimeoutMs: z.number().positive().optional(),
|
|
556
569
|
requestTimeoutMs: z.number().positive().optional(),
|
|
570
|
+
callTimeoutMs: z.number().positive().optional(),
|
|
557
571
|
quiet: z.boolean().optional()
|
|
558
572
|
});
|
|
573
|
+
function isLocalhost(hostname) {
|
|
574
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
575
|
+
}
|
|
559
576
|
var HttpConfigSchema = z.object({
|
|
560
577
|
transport: z.literal("http"),
|
|
561
|
-
serverUrl: z.string().url("serverUrl must be a valid URL")
|
|
578
|
+
serverUrl: z.string().url("serverUrl must be a valid URL").refine((url) => {
|
|
579
|
+
let parsed;
|
|
580
|
+
try {
|
|
581
|
+
parsed = new URL(url);
|
|
582
|
+
} catch {
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
if (parsed.protocol === "http:" && !isLocalhost(parsed.hostname)) {
|
|
586
|
+
console.warn(
|
|
587
|
+
`[mcp-server-tester] serverUrl uses http:// for non-localhost address "${parsed.hostname}". This transmits tokens unencrypted. Use https:// for remote servers.`
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
return true;
|
|
591
|
+
}),
|
|
562
592
|
headers: z.record(z.string()).optional(),
|
|
563
593
|
capabilities: MCPHostCapabilitiesSchema.optional(),
|
|
564
594
|
connectTimeoutMs: z.number().positive().optional(),
|
|
565
595
|
requestTimeoutMs: z.number().positive().optional(),
|
|
566
|
-
|
|
596
|
+
callTimeoutMs: z.number().positive().optional(),
|
|
597
|
+
auth: MCPAuthConfigSchema.optional(),
|
|
598
|
+
proxy: z.object({
|
|
599
|
+
url: z.string().url("proxy.url must be a valid URL")
|
|
600
|
+
}).optional(),
|
|
601
|
+
retryAttempts: z.number().int().min(0).optional(),
|
|
602
|
+
tls: z.object({
|
|
603
|
+
ca: z.string().optional(),
|
|
604
|
+
cert: z.string().optional(),
|
|
605
|
+
key: z.string().optional(),
|
|
606
|
+
rejectUnauthorized: z.boolean().optional()
|
|
607
|
+
}).optional()
|
|
567
608
|
});
|
|
568
609
|
var MCPConfigSchema = z.discriminatedUnion("transport", [
|
|
569
610
|
StdioConfigSchema,
|
|
@@ -573,26 +614,241 @@ function validateMCPConfig(config) {
|
|
|
573
614
|
return MCPConfigSchema.parse(config);
|
|
574
615
|
}
|
|
575
616
|
function isStdioConfig(config) {
|
|
576
|
-
return config.transport === "stdio"
|
|
617
|
+
return config.transport === "stdio";
|
|
577
618
|
}
|
|
578
619
|
function isHttpConfig(config) {
|
|
579
|
-
return config.transport === "http"
|
|
620
|
+
return config.transport === "http";
|
|
580
621
|
}
|
|
581
622
|
var NAMESPACE = "mcp-server-tester";
|
|
582
623
|
var debugClient = createDebug(`${NAMESPACE}:client`);
|
|
583
624
|
createDebug(`${NAMESPACE}:oauth`);
|
|
584
625
|
createDebug(`${NAMESPACE}:eval`);
|
|
626
|
+
var debugHttp = createDebug(`${NAMESPACE}:http`);
|
|
627
|
+
var debug = createDebug("mcp-server-tester:oauth-flow");
|
|
628
|
+
async function generatePKCE() {
|
|
629
|
+
const codeVerifier = oauth.generateRandomCodeVerifier();
|
|
630
|
+
const codeChallenge = await oauth.calculatePKCECodeChallenge(codeVerifier);
|
|
631
|
+
return {
|
|
632
|
+
codeVerifier,
|
|
633
|
+
codeChallenge
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
function generateState() {
|
|
637
|
+
return oauth.generateRandomState();
|
|
638
|
+
}
|
|
639
|
+
function buildAuthorizationUrl(config) {
|
|
640
|
+
const authorizationEndpoint = config.authServer.server.authorization_endpoint;
|
|
641
|
+
if (!authorizationEndpoint) {
|
|
642
|
+
throw new Error(
|
|
643
|
+
"Authorization server does not have an authorization_endpoint"
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
const authorizationUrl = new URL(authorizationEndpoint);
|
|
647
|
+
authorizationUrl.searchParams.set("client_id", config.clientId);
|
|
648
|
+
authorizationUrl.searchParams.set("redirect_uri", config.redirectUri);
|
|
649
|
+
authorizationUrl.searchParams.set("response_type", "code");
|
|
650
|
+
authorizationUrl.searchParams.set("scope", config.scopes.join(" "));
|
|
651
|
+
authorizationUrl.searchParams.set("code_challenge", config.codeChallenge);
|
|
652
|
+
authorizationUrl.searchParams.set("code_challenge_method", "S256");
|
|
653
|
+
authorizationUrl.searchParams.set("state", config.state);
|
|
654
|
+
if (config.resource) {
|
|
655
|
+
authorizationUrl.searchParams.set("resource", config.resource);
|
|
656
|
+
}
|
|
657
|
+
return authorizationUrl;
|
|
658
|
+
}
|
|
659
|
+
async function exchangeCodeForTokens(config) {
|
|
660
|
+
const client = {
|
|
661
|
+
client_id: config.clientId,
|
|
662
|
+
token_endpoint_auth_method: config.clientSecret ? "client_secret_basic" : "none"
|
|
663
|
+
};
|
|
664
|
+
const clientAuth = config.clientSecret ? oauth.ClientSecretBasic(config.clientSecret) : oauth.None();
|
|
665
|
+
const callbackUrl = new URL(config.redirectUri);
|
|
666
|
+
callbackUrl.searchParams.set("code", config.code);
|
|
667
|
+
callbackUrl.searchParams.set("state", config.state);
|
|
668
|
+
const validatedParams = oauth.validateAuthResponse(
|
|
669
|
+
config.authServer.server,
|
|
670
|
+
client,
|
|
671
|
+
callbackUrl,
|
|
672
|
+
config.state
|
|
673
|
+
);
|
|
674
|
+
const response = await oauth.authorizationCodeGrantRequest(
|
|
675
|
+
config.authServer.server,
|
|
676
|
+
client,
|
|
677
|
+
clientAuth,
|
|
678
|
+
validatedParams,
|
|
679
|
+
config.redirectUri,
|
|
680
|
+
config.codeVerifier
|
|
681
|
+
);
|
|
682
|
+
const result = await oauth.processAuthorizationCodeResponse(
|
|
683
|
+
config.authServer.server,
|
|
684
|
+
client,
|
|
685
|
+
response
|
|
686
|
+
);
|
|
687
|
+
return {
|
|
688
|
+
accessToken: result.access_token,
|
|
689
|
+
tokenType: result.token_type,
|
|
690
|
+
expiresIn: result.expires_in,
|
|
691
|
+
refreshToken: result.refresh_token,
|
|
692
|
+
scope: result.scope
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
async function refreshAccessToken(config) {
|
|
696
|
+
const client = {
|
|
697
|
+
client_id: config.clientId,
|
|
698
|
+
token_endpoint_auth_method: config.clientSecret ? "client_secret_basic" : "none"
|
|
699
|
+
};
|
|
700
|
+
const clientAuth = config.clientSecret ? oauth.ClientSecretBasic(config.clientSecret) : oauth.None();
|
|
701
|
+
const response = await oauth.refreshTokenGrantRequest(
|
|
702
|
+
config.authServer.server,
|
|
703
|
+
client,
|
|
704
|
+
clientAuth,
|
|
705
|
+
config.refreshToken
|
|
706
|
+
);
|
|
707
|
+
if (!response.ok) {
|
|
708
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
709
|
+
let errorMessage = `Token refresh failed: ${response.status} ${response.statusText}`;
|
|
710
|
+
try {
|
|
711
|
+
if (contentType.includes("application/json")) {
|
|
712
|
+
const errorBody = await response.clone().json();
|
|
713
|
+
if (errorBody.error) {
|
|
714
|
+
errorMessage = `Token refresh failed: ${errorBody.error}`;
|
|
715
|
+
if (errorBody.error_description) {
|
|
716
|
+
errorMessage += ` - ${errorBody.error_description}`;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
} else {
|
|
720
|
+
const textBody = await response.clone().text();
|
|
721
|
+
if (textBody) {
|
|
722
|
+
errorMessage = `Token refresh failed: ${response.status} - ${textBody}`;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
} catch {
|
|
726
|
+
}
|
|
727
|
+
throw new Error(errorMessage);
|
|
728
|
+
}
|
|
729
|
+
const result = await oauth.processRefreshTokenResponse(
|
|
730
|
+
config.authServer.server,
|
|
731
|
+
client,
|
|
732
|
+
response
|
|
733
|
+
);
|
|
734
|
+
return {
|
|
735
|
+
accessToken: result.access_token,
|
|
736
|
+
tokenType: result.token_type,
|
|
737
|
+
expiresIn: result.expires_in,
|
|
738
|
+
refreshToken: result.refresh_token,
|
|
739
|
+
scope: result.scope
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
async function performClientCredentialsFlow(config) {
|
|
743
|
+
const tokenEndpointUrl = new URL(config.tokenEndpoint);
|
|
744
|
+
const authServer = {
|
|
745
|
+
issuer: tokenEndpointUrl.origin,
|
|
746
|
+
token_endpoint: config.tokenEndpoint
|
|
747
|
+
};
|
|
748
|
+
const client = {
|
|
749
|
+
client_id: config.clientId
|
|
750
|
+
};
|
|
751
|
+
const clientAuth = oauth.ClientSecretBasic(config.clientSecret);
|
|
752
|
+
const parameters = {};
|
|
753
|
+
if (config.scopes && config.scopes.length > 0) {
|
|
754
|
+
parameters["scope"] = config.scopes.join(" ");
|
|
755
|
+
}
|
|
756
|
+
const response = await oauth.clientCredentialsGrantRequest(
|
|
757
|
+
authServer,
|
|
758
|
+
client,
|
|
759
|
+
clientAuth,
|
|
760
|
+
parameters
|
|
761
|
+
);
|
|
762
|
+
const result = await oauth.processClientCredentialsResponse(
|
|
763
|
+
authServer,
|
|
764
|
+
client,
|
|
765
|
+
response
|
|
766
|
+
);
|
|
767
|
+
const requestedScopes = new Set(
|
|
768
|
+
config.scopes && config.scopes.length > 0 ? config.scopes : []
|
|
769
|
+
);
|
|
770
|
+
const grantedScopes = new Set(
|
|
771
|
+
(result.scope ?? "").split(" ").filter(Boolean)
|
|
772
|
+
);
|
|
773
|
+
const missingScopes = [...requestedScopes].filter(
|
|
774
|
+
(s) => !grantedScopes.has(s)
|
|
775
|
+
);
|
|
776
|
+
if (missingScopes.length > 0 && requestedScopes.size > 0 && grantedScopes.size > 0) {
|
|
777
|
+
debug(
|
|
778
|
+
"[oauth] Warning: Token server granted fewer scopes than requested. Missing: %s",
|
|
779
|
+
missingScopes.join(", ")
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
return {
|
|
783
|
+
accessToken: result.access_token,
|
|
784
|
+
tokenType: result.token_type,
|
|
785
|
+
expiresIn: result.expires_in,
|
|
786
|
+
scope: result.scope
|
|
787
|
+
};
|
|
788
|
+
}
|
|
585
789
|
|
|
586
790
|
// src/mcp/clientFactory.ts
|
|
791
|
+
function getRetryAfterDelayMs(err) {
|
|
792
|
+
const response = err?.response;
|
|
793
|
+
const retryAfter = response?.headers?.get?.("Retry-After");
|
|
794
|
+
if (retryAfter) {
|
|
795
|
+
const seconds = parseInt(retryAfter, 10);
|
|
796
|
+
if (!isNaN(seconds)) return seconds * 1e3;
|
|
797
|
+
}
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
function isRateLimitError(err) {
|
|
801
|
+
const response = err?.response;
|
|
802
|
+
return response?.status === 429;
|
|
803
|
+
}
|
|
804
|
+
function isTransientNetworkError(err) {
|
|
805
|
+
if (!(err instanceof Error)) return false;
|
|
806
|
+
const msg = err.message.toLowerCase();
|
|
807
|
+
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");
|
|
808
|
+
}
|
|
809
|
+
function isRetryableError(err) {
|
|
810
|
+
return isTransientNetworkError(err) || isRateLimitError(err);
|
|
811
|
+
}
|
|
812
|
+
async function retryWithBackoff(fn, maxAttempts) {
|
|
813
|
+
let lastErr;
|
|
814
|
+
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
|
|
815
|
+
try {
|
|
816
|
+
return await fn();
|
|
817
|
+
} catch (err) {
|
|
818
|
+
lastErr = err;
|
|
819
|
+
if (attempt < maxAttempts && isRetryableError(err)) {
|
|
820
|
+
const retryAfterMs = getRetryAfterDelayMs(err);
|
|
821
|
+
const delayMs = retryAfterMs !== null ? retryAfterMs : Math.min(1e3 * 2 ** attempt, 3e4);
|
|
822
|
+
debugClient(
|
|
823
|
+
"Retryable error on attempt %d/%d, retrying in %dms: %s",
|
|
824
|
+
attempt + 1,
|
|
825
|
+
maxAttempts + 1,
|
|
826
|
+
delayMs,
|
|
827
|
+
err.message
|
|
828
|
+
);
|
|
829
|
+
await new Promise((resolve3) => setTimeout(resolve3, delayMs));
|
|
830
|
+
} else {
|
|
831
|
+
throw err;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
throw lastErr;
|
|
836
|
+
}
|
|
837
|
+
var agentRegistry = /* @__PURE__ */ new WeakMap();
|
|
587
838
|
async function createMCPClientForConfig(config, options) {
|
|
588
839
|
const validatedConfig = validateMCPConfig(config);
|
|
589
840
|
const client = new Client(
|
|
590
841
|
{
|
|
591
842
|
name: "@gleanwork/mcp-server-tester",
|
|
592
|
-
version:
|
|
843
|
+
version: package_default.version
|
|
593
844
|
},
|
|
594
845
|
{
|
|
595
|
-
capabilities:
|
|
846
|
+
capabilities: {
|
|
847
|
+
...validatedConfig.capabilities ?? {},
|
|
848
|
+
// Only advertise sampling if a handler has been registered;
|
|
849
|
+
// declaring sampling capability without a handler violates the MCP spec
|
|
850
|
+
sampling: void 0
|
|
851
|
+
}
|
|
596
852
|
}
|
|
597
853
|
);
|
|
598
854
|
if (isStdioConfig(validatedConfig)) {
|
|
@@ -608,26 +864,126 @@ async function createMCPClientForConfig(config, options) {
|
|
|
608
864
|
args: validatedConfig.args,
|
|
609
865
|
cwd: validatedConfig.cwd
|
|
610
866
|
});
|
|
611
|
-
await client.connect(
|
|
867
|
+
await client.connect(
|
|
868
|
+
transport,
|
|
869
|
+
validatedConfig.connectTimeoutMs !== void 0 ? { timeout: validatedConfig.connectTimeoutMs } : void 0
|
|
870
|
+
);
|
|
612
871
|
} else if (isHttpConfig(validatedConfig)) {
|
|
613
872
|
const headers = { ...validatedConfig.headers };
|
|
873
|
+
if (validatedConfig.auth?.clientCredentials && true) {
|
|
874
|
+
const ccConfig = validatedConfig.auth.clientCredentials;
|
|
875
|
+
const clientId = ccConfig.clientId ?? process.env["MCP_CLIENT_ID"];
|
|
876
|
+
const clientSecret = ccConfig.clientSecret ?? process.env["MCP_CLIENT_SECRET"];
|
|
877
|
+
if (!clientId || !clientSecret) {
|
|
878
|
+
throw new Error(
|
|
879
|
+
"Client credentials require clientId/clientSecret in config or MCP_CLIENT_ID/MCP_CLIENT_SECRET env vars"
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
if (!ccConfig.tokenEndpoint) {
|
|
883
|
+
throw new Error(
|
|
884
|
+
"Client credentials require tokenEndpoint in auth.clientCredentials config"
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
debugClient("Fetching token via client credentials grant");
|
|
888
|
+
const tokenResult = await performClientCredentialsFlow({
|
|
889
|
+
tokenEndpoint: ccConfig.tokenEndpoint,
|
|
890
|
+
clientId,
|
|
891
|
+
clientSecret,
|
|
892
|
+
scopes: ccConfig.scopes
|
|
893
|
+
});
|
|
894
|
+
headers.Authorization = `Bearer ${tokenResult.accessToken}`;
|
|
895
|
+
}
|
|
614
896
|
if (validatedConfig.auth?.accessToken && true) {
|
|
615
897
|
headers.Authorization = `Bearer ${validatedConfig.auth.accessToken}`;
|
|
616
898
|
}
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
899
|
+
const url = new URL(validatedConfig.serverUrl);
|
|
900
|
+
let requestInit = Object.keys(headers).length > 0 ? { headers } : void 0;
|
|
901
|
+
const proxyUrl = validatedConfig.proxy?.url ?? process.env["HTTPS_PROXY"] ?? process.env["HTTP_PROXY"];
|
|
902
|
+
if (proxyUrl) {
|
|
903
|
+
const proxyAgent = new ProxyAgent(proxyUrl);
|
|
904
|
+
try {
|
|
905
|
+
const sanitized = new URL(proxyUrl);
|
|
906
|
+
debugClient(
|
|
907
|
+
"Using proxy: %s://%s:%s",
|
|
908
|
+
sanitized.protocol.slice(0, -1),
|
|
909
|
+
sanitized.hostname,
|
|
910
|
+
sanitized.port
|
|
911
|
+
);
|
|
912
|
+
} catch {
|
|
913
|
+
debugClient("Using proxy (unparseable URL)");
|
|
623
914
|
}
|
|
624
|
-
|
|
915
|
+
requestInit = {
|
|
916
|
+
...requestInit,
|
|
917
|
+
dispatcher: proxyAgent
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
if (validatedConfig.tls) {
|
|
921
|
+
const tlsCfg = validatedConfig.tls;
|
|
922
|
+
try {
|
|
923
|
+
const dispatcher = new Agent({
|
|
924
|
+
connect: {
|
|
925
|
+
...tlsCfg.ca && { ca: readFileSync(tlsCfg.ca) },
|
|
926
|
+
...tlsCfg.cert && { cert: readFileSync(tlsCfg.cert) },
|
|
927
|
+
...tlsCfg.key && { key: readFileSync(tlsCfg.key) },
|
|
928
|
+
rejectUnauthorized: tlsCfg.rejectUnauthorized ?? true
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
agentRegistry.set(client, dispatcher);
|
|
932
|
+
requestInit = {
|
|
933
|
+
...requestInit,
|
|
934
|
+
dispatcher
|
|
935
|
+
};
|
|
936
|
+
debugClient("TLS configuration applied");
|
|
937
|
+
} catch (error) {
|
|
938
|
+
const filePath = tlsCfg.ca ?? tlsCfg.cert ?? tlsCfg.key;
|
|
939
|
+
const fileType = tlsCfg.ca ? "CA certificate" : tlsCfg.cert ? "client certificate" : "client key";
|
|
940
|
+
throw new Error(
|
|
941
|
+
`Failed to load TLS ${fileType} from ${filePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
} else if (proxyUrl) {
|
|
945
|
+
const existingDispatcher = requestInit?.dispatcher;
|
|
946
|
+
if (existingDispatcher) {
|
|
947
|
+
agentRegistry.set(client, existingDispatcher);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
625
950
|
debugClient("Connecting via HTTP: %O", {
|
|
626
951
|
serverUrl: validatedConfig.serverUrl,
|
|
627
952
|
headers: Object.keys(headers).length > 0 ? Object.keys(headers) : void 0,
|
|
628
953
|
hasAuthProvider: false
|
|
629
954
|
});
|
|
630
|
-
|
|
955
|
+
debugHttp("Connecting to %s", validatedConfig.serverUrl);
|
|
956
|
+
if (Object.keys(headers).length > 0) {
|
|
957
|
+
debugHttp("Request header names: %O", Object.keys(headers));
|
|
958
|
+
}
|
|
959
|
+
const retryAttempts = validatedConfig.retryAttempts ?? 0;
|
|
960
|
+
const connectOptions = validatedConfig.connectTimeoutMs !== void 0 ? { timeout: validatedConfig.connectTimeoutMs } : void 0;
|
|
961
|
+
await retryWithBackoff(async () => {
|
|
962
|
+
try {
|
|
963
|
+
debugHttp("Attempting transport: streamableHttp");
|
|
964
|
+
const streamableTransport = new StreamableHTTPClientTransport(url, {
|
|
965
|
+
requestInit,
|
|
966
|
+
authProvider: options?.authProvider
|
|
967
|
+
});
|
|
968
|
+
await client.connect(streamableTransport, connectOptions);
|
|
969
|
+
debugClient("Connected via Streamable HTTP");
|
|
970
|
+
debugHttp("Connection established via streamableHttp");
|
|
971
|
+
} catch (err) {
|
|
972
|
+
debugHttp(
|
|
973
|
+
"streamableHttp failed (%s), falling back to SSE",
|
|
974
|
+
err.message
|
|
975
|
+
);
|
|
976
|
+
debugClient("Streamable HTTP failed, falling back to SSE transport");
|
|
977
|
+
debugHttp("Attempting transport: sse");
|
|
978
|
+
const sseTransport = new SSEClientTransport(url, {
|
|
979
|
+
requestInit,
|
|
980
|
+
authProvider: options?.authProvider
|
|
981
|
+
});
|
|
982
|
+
await client.connect(sseTransport, connectOptions);
|
|
983
|
+
debugClient("Connected via SSE");
|
|
984
|
+
debugHttp("Connection established via sse");
|
|
985
|
+
}
|
|
986
|
+
}, retryAttempts);
|
|
631
987
|
}
|
|
632
988
|
debugClient("Connected successfully");
|
|
633
989
|
const serverInfo = client.getServerVersion();
|
|
@@ -642,6 +998,19 @@ async function closeMCPClient(client) {
|
|
|
642
998
|
} catch (error) {
|
|
643
999
|
console.error("[MCP] Error closing client:", error);
|
|
644
1000
|
throw error;
|
|
1001
|
+
} finally {
|
|
1002
|
+
const agent = agentRegistry.get(client);
|
|
1003
|
+
if (agent) {
|
|
1004
|
+
agentRegistry.delete(client);
|
|
1005
|
+
try {
|
|
1006
|
+
await agent.close();
|
|
1007
|
+
} catch (agentError) {
|
|
1008
|
+
debugClient(
|
|
1009
|
+
"Error closing undici agent: %s",
|
|
1010
|
+
agentError.message
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
645
1014
|
}
|
|
646
1015
|
}
|
|
647
1016
|
var ENV_VAR_NAMES = {
|
|
@@ -820,119 +1189,27 @@ var FileOAuthStorage = class {
|
|
|
820
1189
|
}
|
|
821
1190
|
}
|
|
822
1191
|
};
|
|
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);
|
|
1192
|
+
function isLocalhostUrl(url) {
|
|
1193
|
+
try {
|
|
1194
|
+
const parsed = new URL(url);
|
|
1195
|
+
const h = parsed.hostname;
|
|
1196
|
+
return h === "localhost" || h === "127.0.0.1" || h === "::1";
|
|
1197
|
+
} catch {
|
|
1198
|
+
return false;
|
|
851
1199
|
}
|
|
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
1200
|
}
|
|
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 {
|
|
1201
|
+
function validateAuthServerEndpoints(authServer) {
|
|
1202
|
+
const endpoints = [
|
|
1203
|
+
{ name: "authorization_endpoint", url: authServer.authorization_endpoint },
|
|
1204
|
+
{ name: "token_endpoint", url: authServer.token_endpoint }
|
|
1205
|
+
];
|
|
1206
|
+
for (const { name, url } of endpoints) {
|
|
1207
|
+
if (url && !url.startsWith("https://") && !isLocalhostUrl(url)) {
|
|
1208
|
+
throw new Error(
|
|
1209
|
+
`OAuth discovery returned an insecure ${name}: "${url}". Only HTTPS endpoints are permitted for OAuth flows to prevent token interception.`
|
|
1210
|
+
);
|
|
921
1211
|
}
|
|
922
|
-
throw new Error(errorMessage);
|
|
923
1212
|
}
|
|
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
1213
|
}
|
|
937
1214
|
var MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
938
1215
|
async function discoverProtectedResource(mcpServerUrl) {
|
|
@@ -1002,6 +1279,7 @@ async function discoverAuthorizationServer(authServerUrl) {
|
|
|
1002
1279
|
})
|
|
1003
1280
|
});
|
|
1004
1281
|
const metadata = await oauth.processDiscoveryResponse(issuer, response);
|
|
1282
|
+
validateAuthServerEndpoints(metadata);
|
|
1005
1283
|
return {
|
|
1006
1284
|
server: metadata,
|
|
1007
1285
|
issuer: authServerUrl
|
|
@@ -1009,7 +1287,7 @@ async function discoverAuthorizationServer(authServerUrl) {
|
|
|
1009
1287
|
}
|
|
1010
1288
|
|
|
1011
1289
|
// src/auth/cli.ts
|
|
1012
|
-
var
|
|
1290
|
+
var debug2 = createDebug("mcp-server-tester:cli-oauth");
|
|
1013
1291
|
var DEFAULT_TIMEOUT_MS = 3e5;
|
|
1014
1292
|
var DEFAULT_CLIENT_NAME = "@gleanwork/mcp-server-tester";
|
|
1015
1293
|
var DEFAULT_METADATA_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -1035,7 +1313,7 @@ var CLIOAuthClient = class {
|
|
|
1035
1313
|
async getAccessToken() {
|
|
1036
1314
|
const envTokens = loadTokensFromEnv();
|
|
1037
1315
|
if (envTokens) {
|
|
1038
|
-
|
|
1316
|
+
debug2("Using tokens from environment variables");
|
|
1039
1317
|
return {
|
|
1040
1318
|
accessToken: envTokens.accessToken,
|
|
1041
1319
|
tokenType: envTokens.tokenType,
|
|
@@ -1048,7 +1326,7 @@ var CLIOAuthClient = class {
|
|
|
1048
1326
|
if (storedTokens?.accessToken) {
|
|
1049
1327
|
const isValid = await this.storage.hasValidToken();
|
|
1050
1328
|
if (isValid) {
|
|
1051
|
-
|
|
1329
|
+
debug2("Using cached tokens from storage");
|
|
1052
1330
|
return {
|
|
1053
1331
|
accessToken: storedTokens.accessToken,
|
|
1054
1332
|
tokenType: storedTokens.tokenType,
|
|
@@ -1058,7 +1336,7 @@ var CLIOAuthClient = class {
|
|
|
1058
1336
|
};
|
|
1059
1337
|
}
|
|
1060
1338
|
if (storedTokens.refreshToken) {
|
|
1061
|
-
|
|
1339
|
+
debug2("Token expired, attempting refresh");
|
|
1062
1340
|
try {
|
|
1063
1341
|
const refreshedTokens = await this.refreshStoredToken(storedTokens);
|
|
1064
1342
|
return {
|
|
@@ -1069,11 +1347,11 @@ var CLIOAuthClient = class {
|
|
|
1069
1347
|
fromEnv: false
|
|
1070
1348
|
};
|
|
1071
1349
|
} catch (error) {
|
|
1072
|
-
|
|
1350
|
+
debug2("Token refresh failed, will re-authenticate:", error);
|
|
1073
1351
|
}
|
|
1074
1352
|
}
|
|
1075
1353
|
}
|
|
1076
|
-
|
|
1354
|
+
debug2("Performing full OAuth authentication");
|
|
1077
1355
|
return this.authenticate();
|
|
1078
1356
|
}
|
|
1079
1357
|
/**
|
|
@@ -1089,7 +1367,7 @@ var CLIOAuthClient = class {
|
|
|
1089
1367
|
async tryGetAccessToken() {
|
|
1090
1368
|
const envTokens = loadTokensFromEnv();
|
|
1091
1369
|
if (envTokens) {
|
|
1092
|
-
|
|
1370
|
+
debug2("Using tokens from environment variables");
|
|
1093
1371
|
return {
|
|
1094
1372
|
accessToken: envTokens.accessToken,
|
|
1095
1373
|
tokenType: envTokens.tokenType,
|
|
@@ -1102,7 +1380,7 @@ var CLIOAuthClient = class {
|
|
|
1102
1380
|
if (storedTokens?.accessToken) {
|
|
1103
1381
|
const isValid = await this.storage.hasValidToken();
|
|
1104
1382
|
if (isValid) {
|
|
1105
|
-
|
|
1383
|
+
debug2("Using cached tokens from storage");
|
|
1106
1384
|
return {
|
|
1107
1385
|
accessToken: storedTokens.accessToken,
|
|
1108
1386
|
tokenType: storedTokens.tokenType,
|
|
@@ -1112,7 +1390,7 @@ var CLIOAuthClient = class {
|
|
|
1112
1390
|
};
|
|
1113
1391
|
}
|
|
1114
1392
|
if (storedTokens.refreshToken) {
|
|
1115
|
-
|
|
1393
|
+
debug2("Token expired, attempting refresh");
|
|
1116
1394
|
try {
|
|
1117
1395
|
const refreshedTokens = await this.refreshStoredToken(storedTokens);
|
|
1118
1396
|
return {
|
|
@@ -1123,12 +1401,12 @@ var CLIOAuthClient = class {
|
|
|
1123
1401
|
fromEnv: false
|
|
1124
1402
|
};
|
|
1125
1403
|
} catch (error) {
|
|
1126
|
-
|
|
1404
|
+
debug2("Token refresh failed:", error);
|
|
1127
1405
|
return null;
|
|
1128
1406
|
}
|
|
1129
1407
|
}
|
|
1130
1408
|
}
|
|
1131
|
-
|
|
1409
|
+
debug2("No valid token available");
|
|
1132
1410
|
return null;
|
|
1133
1411
|
}
|
|
1134
1412
|
/**
|
|
@@ -1163,7 +1441,7 @@ var CLIOAuthClient = class {
|
|
|
1163
1441
|
*/
|
|
1164
1442
|
async clearCredentials() {
|
|
1165
1443
|
await this.storage.deleteTokens();
|
|
1166
|
-
|
|
1444
|
+
debug2("Cleared stored credentials");
|
|
1167
1445
|
}
|
|
1168
1446
|
/**
|
|
1169
1447
|
* Discover protected resource and authorization server
|
|
@@ -1173,12 +1451,12 @@ var CLIOAuthClient = class {
|
|
|
1173
1451
|
if (cachedMetadata) {
|
|
1174
1452
|
const age = Date.now() - cachedMetadata.discoveredAt;
|
|
1175
1453
|
if (age < DEFAULT_METADATA_TTL_MS) {
|
|
1176
|
-
|
|
1177
|
-
|
|
1454
|
+
debug2("Using cached server metadata (age: %dms)", age);
|
|
1455
|
+
debug2(
|
|
1178
1456
|
"Cached protected resource scopes: %O",
|
|
1179
1457
|
cachedMetadata.protectedResource.scopes_supported
|
|
1180
1458
|
);
|
|
1181
|
-
|
|
1459
|
+
debug2(
|
|
1182
1460
|
"Cached auth server scopes: %O",
|
|
1183
1461
|
cachedMetadata.authServer.server.scopes_supported
|
|
1184
1462
|
);
|
|
@@ -1187,12 +1465,12 @@ var CLIOAuthClient = class {
|
|
|
1187
1465
|
authServer: cachedMetadata.authServer
|
|
1188
1466
|
};
|
|
1189
1467
|
}
|
|
1190
|
-
|
|
1468
|
+
debug2("Cached server metadata is stale (age: %dms), re-discovering", age);
|
|
1191
1469
|
}
|
|
1192
|
-
|
|
1470
|
+
debug2("Discovering protected resource:", this.config.mcpServerUrl);
|
|
1193
1471
|
const prResult = await discoverProtectedResource(this.config.mcpServerUrl);
|
|
1194
|
-
|
|
1195
|
-
|
|
1472
|
+
debug2("Found protected resource:", prResult.metadata.resource);
|
|
1473
|
+
debug2(
|
|
1196
1474
|
"Protected resource scopes_supported: %O",
|
|
1197
1475
|
prResult.metadata.scopes_supported
|
|
1198
1476
|
);
|
|
@@ -1202,10 +1480,10 @@ var CLIOAuthClient = class {
|
|
|
1202
1480
|
"No authorization servers found in protected resource metadata"
|
|
1203
1481
|
);
|
|
1204
1482
|
}
|
|
1205
|
-
|
|
1483
|
+
debug2("Discovering authorization server:", authServerUrl);
|
|
1206
1484
|
const authServer = await discoverAuthorizationServer(authServerUrl);
|
|
1207
|
-
|
|
1208
|
-
|
|
1485
|
+
debug2("Found authorization server:", authServer.issuer);
|
|
1486
|
+
debug2(
|
|
1209
1487
|
"Auth server scopes_supported: %O",
|
|
1210
1488
|
authServer.server.scopes_supported
|
|
1211
1489
|
);
|
|
@@ -1225,7 +1503,7 @@ var CLIOAuthClient = class {
|
|
|
1225
1503
|
*/
|
|
1226
1504
|
async getOrRegisterClient(authServer) {
|
|
1227
1505
|
if (this.config.clientId) {
|
|
1228
|
-
|
|
1506
|
+
debug2("Using pre-configured client ID");
|
|
1229
1507
|
return {
|
|
1230
1508
|
clientId: this.config.clientId,
|
|
1231
1509
|
clientSecret: this.config.clientSecret
|
|
@@ -1233,10 +1511,10 @@ var CLIOAuthClient = class {
|
|
|
1233
1511
|
}
|
|
1234
1512
|
const cachedClient = await this.storage.loadClient();
|
|
1235
1513
|
if (cachedClient?.clientId) {
|
|
1236
|
-
|
|
1514
|
+
debug2("Using cached client registration");
|
|
1237
1515
|
return cachedClient;
|
|
1238
1516
|
}
|
|
1239
|
-
|
|
1517
|
+
debug2("Registering new client via DCR");
|
|
1240
1518
|
const client = await this.registerClient(authServer);
|
|
1241
1519
|
await this.storage.saveClient(client);
|
|
1242
1520
|
return client;
|
|
@@ -1274,7 +1552,7 @@ ${errorText}`
|
|
|
1274
1552
|
);
|
|
1275
1553
|
}
|
|
1276
1554
|
const data = await response.json();
|
|
1277
|
-
|
|
1555
|
+
debug2("Client registered:", data.client_id);
|
|
1278
1556
|
return {
|
|
1279
1557
|
clientId: data.client_id,
|
|
1280
1558
|
clientSecret: data.client_secret,
|
|
@@ -1292,17 +1570,17 @@ ${errorText}`
|
|
|
1292
1570
|
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
1293
1571
|
try {
|
|
1294
1572
|
const requestedScopes = this.config.scopes ?? protectedResource.scopes_supported ?? authServer.server.scopes_supported ?? ["openid"];
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1573
|
+
debug2("Scope resolution:");
|
|
1574
|
+
debug2(" - User config scopes: %O", this.config.scopes);
|
|
1575
|
+
debug2(
|
|
1298
1576
|
" - Protected resource scopes_supported: %O",
|
|
1299
1577
|
protectedResource.scopes_supported
|
|
1300
1578
|
);
|
|
1301
|
-
|
|
1579
|
+
debug2(
|
|
1302
1580
|
" - Auth server scopes_supported: %O",
|
|
1303
1581
|
authServer.server.scopes_supported
|
|
1304
1582
|
);
|
|
1305
|
-
|
|
1583
|
+
debug2(" - Final requested scopes: %O", requestedScopes);
|
|
1306
1584
|
const authUrl = buildAuthorizationUrl({
|
|
1307
1585
|
authServer,
|
|
1308
1586
|
clientId: client.clientId,
|
|
@@ -1312,16 +1590,19 @@ ${errorText}`
|
|
|
1312
1590
|
state,
|
|
1313
1591
|
resource: protectedResource.resource
|
|
1314
1592
|
});
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1593
|
+
debug2(
|
|
1594
|
+
"Authorization URL (base): %s",
|
|
1595
|
+
`${authUrl.origin}${authUrl.pathname}`
|
|
1596
|
+
);
|
|
1597
|
+
debug2("Authorization URL params:");
|
|
1598
|
+
debug2(" - client_id: %s", authUrl.searchParams.get("client_id"));
|
|
1599
|
+
debug2(" - redirect_uri: %s", authUrl.searchParams.get("redirect_uri"));
|
|
1600
|
+
debug2(" - scope: %s", authUrl.searchParams.get("scope"));
|
|
1601
|
+
debug2(" - resource: %s", authUrl.searchParams.get("resource"));
|
|
1321
1602
|
await this.openBrowserOrPrintUrl(authUrl);
|
|
1322
|
-
|
|
1603
|
+
debug2("Waiting for OAuth callback...");
|
|
1323
1604
|
const code = await codePromise;
|
|
1324
|
-
|
|
1605
|
+
debug2("Received authorization code");
|
|
1325
1606
|
const tokenResult = await exchangeCodeForTokens({
|
|
1326
1607
|
authServer,
|
|
1327
1608
|
clientId: client.clientId,
|
|
@@ -1359,14 +1640,14 @@ ${errorText}`
|
|
|
1359
1640
|
let clientId;
|
|
1360
1641
|
let clientSecret;
|
|
1361
1642
|
if (storedTokens.clientId) {
|
|
1362
|
-
|
|
1643
|
+
debug2("Using clientId from stored tokens for refresh");
|
|
1363
1644
|
clientId = storedTokens.clientId;
|
|
1364
1645
|
const storedClient = await this.storage.loadClient();
|
|
1365
1646
|
if (storedClient?.clientId === clientId) {
|
|
1366
1647
|
clientSecret = storedClient.clientSecret;
|
|
1367
1648
|
}
|
|
1368
1649
|
} else {
|
|
1369
|
-
|
|
1650
|
+
debug2(
|
|
1370
1651
|
"No clientId in stored tokens, falling back to stored client (legacy behavior)"
|
|
1371
1652
|
);
|
|
1372
1653
|
const client = await this.getOrRegisterClient(metadata.authServer);
|
|
@@ -1460,7 +1741,7 @@ ${errorText}`
|
|
|
1460
1741
|
const preferredPort = this.config.callbackPort ?? 0;
|
|
1461
1742
|
server.listen(preferredPort, "127.0.0.1", () => {
|
|
1462
1743
|
const address = server.address();
|
|
1463
|
-
|
|
1744
|
+
debug2("Callback server listening on port", address.port);
|
|
1464
1745
|
resolve3({ port: address.port, codePromise, close: forceClose });
|
|
1465
1746
|
});
|
|
1466
1747
|
server.on("error", (err) => {
|
|
@@ -1484,9 +1765,9 @@ ${errorText}`
|
|
|
1484
1765
|
try {
|
|
1485
1766
|
const open = await import('open');
|
|
1486
1767
|
await open.default(url.toString());
|
|
1487
|
-
|
|
1768
|
+
debug2("Opened browser for authentication");
|
|
1488
1769
|
} catch (error) {
|
|
1489
|
-
|
|
1770
|
+
debug2("Failed to open browser:", error);
|
|
1490
1771
|
console.log("\nFailed to open browser automatically.");
|
|
1491
1772
|
console.log("Please open the following URL manually:\n");
|
|
1492
1773
|
console.log(url.toString() + "\n");
|
|
@@ -1697,7 +1978,7 @@ function isCallToolResult(value) {
|
|
|
1697
1978
|
return false;
|
|
1698
1979
|
}
|
|
1699
1980
|
const v = value;
|
|
1700
|
-
return Array.isArray(v.content)
|
|
1981
|
+
return Array.isArray(v.content);
|
|
1701
1982
|
}
|
|
1702
1983
|
function extractTextFromContentArray(content) {
|
|
1703
1984
|
const textParts = [];
|
|
@@ -2770,7 +3051,7 @@ async function token(serverUrl, options) {
|
|
|
2770
3051
|
|
|
2771
3052
|
// src/cli/index.ts
|
|
2772
3053
|
var program = new Command();
|
|
2773
|
-
program.name("mcp-server-tester").description("CLI tools for MCP server evaluation and testing").version(
|
|
3054
|
+
program.name("mcp-server-tester").description("CLI tools for MCP server evaluation and testing").version(package_default.version);
|
|
2774
3055
|
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
3056
|
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
3057
|
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(
|