@frontmcp/testing 1.7.1 → 1.8.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/client/mcp-test-client.d.ts +6 -2
- package/client/mcp-test-client.d.ts.map +1 -1
- package/client/mcp-test-client.types.d.ts +9 -2
- package/client/mcp-test-client.types.d.ts.map +1 -1
- package/esm/fixtures/index.mjs +545 -454
- package/esm/index.mjs +635 -544
- package/esm/package.json +6 -6
- package/esm/perf/index.mjs +474 -421
- package/fixtures/fixture-types.d.ts +34 -3
- package/fixtures/fixture-types.d.ts.map +1 -1
- package/fixtures/index.js +545 -454
- package/fixtures/test-fixture.d.ts.map +1 -1
- package/index.js +635 -544
- package/package.json +6 -6
- package/perf/index.js +474 -421
- package/transport/streamable-http.transport.d.ts +8 -2
- package/transport/streamable-http.transport.d.ts.map +1 -1
- package/transport/transport.interface.d.ts +8 -1
- package/transport/transport.interface.d.ts.map +1 -1
package/esm/perf/index.mjs
CHANGED
|
@@ -772,233 +772,277 @@ function getMeasurementsForProject(project) {
|
|
|
772
772
|
return getGlobalMeasurementsArray().filter((m) => m.project === project);
|
|
773
773
|
}
|
|
774
774
|
|
|
775
|
-
// libs/testing/src/
|
|
776
|
-
var
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
name: "mcp-ext-apps",
|
|
787
|
-
version: "1.0"
|
|
788
|
-
};
|
|
789
|
-
case "claude":
|
|
790
|
-
return {
|
|
791
|
-
name: "claude-desktop",
|
|
792
|
-
version: "1.0"
|
|
793
|
-
};
|
|
794
|
-
case "cursor":
|
|
795
|
-
return {
|
|
796
|
-
name: "cursor",
|
|
797
|
-
version: "1.0"
|
|
798
|
-
};
|
|
799
|
-
case "continue":
|
|
800
|
-
return {
|
|
801
|
-
name: "continue",
|
|
802
|
-
version: "1.0"
|
|
803
|
-
};
|
|
804
|
-
case "cody":
|
|
805
|
-
return {
|
|
806
|
-
name: "cody",
|
|
807
|
-
version: "1.0"
|
|
808
|
-
};
|
|
809
|
-
case "gemini":
|
|
810
|
-
return {
|
|
811
|
-
name: "gemini",
|
|
812
|
-
version: "1.0"
|
|
813
|
-
};
|
|
814
|
-
case "generic-mcp":
|
|
815
|
-
return {
|
|
816
|
-
name: "generic-mcp-client",
|
|
817
|
-
version: "1.0"
|
|
818
|
-
};
|
|
819
|
-
case "unknown":
|
|
820
|
-
default:
|
|
821
|
-
return {
|
|
822
|
-
name: "mcp-test-client",
|
|
823
|
-
version: "1.0"
|
|
824
|
-
};
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
function getPlatformCapabilities(platform) {
|
|
828
|
-
const baseCapabilities = {
|
|
829
|
-
sampling: {},
|
|
830
|
-
// Include elicitation.form by default for testing elicitation workflows
|
|
831
|
-
// Note: MCP SDK expects form to be an object, not boolean
|
|
832
|
-
elicitation: {
|
|
833
|
-
form: {}
|
|
834
|
-
}
|
|
835
|
-
};
|
|
836
|
-
if (platform === "ext-apps") {
|
|
775
|
+
// libs/testing/src/interceptor/mock-registry.ts
|
|
776
|
+
var DefaultMockRegistry = class {
|
|
777
|
+
mocks = [];
|
|
778
|
+
add(mock) {
|
|
779
|
+
const entry = {
|
|
780
|
+
definition: mock,
|
|
781
|
+
callCount: 0,
|
|
782
|
+
calls: [],
|
|
783
|
+
remainingUses: mock.times ?? Infinity
|
|
784
|
+
};
|
|
785
|
+
this.mocks.push(entry);
|
|
837
786
|
return {
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
787
|
+
remove: () => {
|
|
788
|
+
const index = this.mocks.indexOf(entry);
|
|
789
|
+
if (index !== -1) {
|
|
790
|
+
this.mocks.splice(index, 1);
|
|
842
791
|
}
|
|
843
|
-
}
|
|
792
|
+
},
|
|
793
|
+
callCount: () => entry.callCount,
|
|
794
|
+
calls: () => [...entry.calls]
|
|
844
795
|
};
|
|
845
796
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
// libs/testing/src/client/mcp-test-client.builder.ts
|
|
850
|
-
var McpTestClientBuilder = class {
|
|
851
|
-
config;
|
|
852
|
-
constructor(config) {
|
|
853
|
-
this.config = { ...config };
|
|
797
|
+
clear() {
|
|
798
|
+
this.mocks = [];
|
|
854
799
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
800
|
+
getAll() {
|
|
801
|
+
return this.mocks.map((e) => e.definition);
|
|
802
|
+
}
|
|
803
|
+
match(request) {
|
|
804
|
+
for (const entry of this.mocks) {
|
|
805
|
+
if (entry.remainingUses <= 0) continue;
|
|
806
|
+
const { definition } = entry;
|
|
807
|
+
if (definition.method !== request.method) continue;
|
|
808
|
+
if (definition.params !== void 0) {
|
|
809
|
+
const params = request.params ?? {};
|
|
810
|
+
if (typeof definition.params === "function") {
|
|
811
|
+
if (!definition.params(params)) continue;
|
|
812
|
+
} else {
|
|
813
|
+
if (!this.paramsMatch(definition.params, params)) continue;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
entry.callCount++;
|
|
817
|
+
entry.calls.push(request);
|
|
818
|
+
entry.remainingUses--;
|
|
819
|
+
return definition;
|
|
820
|
+
}
|
|
821
|
+
return void 0;
|
|
861
822
|
}
|
|
862
823
|
/**
|
|
863
|
-
*
|
|
824
|
+
* Check if request params match the mock params definition
|
|
864
825
|
*/
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
826
|
+
paramsMatch(expected, actual) {
|
|
827
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
828
|
+
if (!(key in actual)) return false;
|
|
829
|
+
const actualValue = actual[key];
|
|
830
|
+
if (Array.isArray(value)) {
|
|
831
|
+
if (!Array.isArray(actualValue)) return false;
|
|
832
|
+
if (value.length !== actualValue.length) return false;
|
|
833
|
+
for (let i = 0; i < value.length; i++) {
|
|
834
|
+
const expectedItem = value[i];
|
|
835
|
+
const actualItem = actualValue[i];
|
|
836
|
+
if (typeof expectedItem === "object" && expectedItem !== null) {
|
|
837
|
+
if (typeof actualItem !== "object" || actualItem === null) return false;
|
|
838
|
+
if (!this.paramsMatch(expectedItem, actualItem)) {
|
|
839
|
+
return false;
|
|
840
|
+
}
|
|
841
|
+
} else if (actualItem !== expectedItem) {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
} else if (typeof value === "object" && value !== null) {
|
|
846
|
+
if (typeof actualValue !== "object" || actualValue === null) return false;
|
|
847
|
+
if (!this.paramsMatch(value, actualValue)) {
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
} else if (actualValue !== value) {
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return true;
|
|
868
855
|
}
|
|
856
|
+
};
|
|
857
|
+
var mockResponse = {
|
|
869
858
|
/**
|
|
870
|
-
*
|
|
859
|
+
* Create a successful JSON-RPC response
|
|
871
860
|
*/
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
861
|
+
success(result, id = 1) {
|
|
862
|
+
return {
|
|
863
|
+
jsonrpc: "2.0",
|
|
864
|
+
id,
|
|
865
|
+
result
|
|
876
866
|
};
|
|
877
|
-
|
|
878
|
-
}
|
|
867
|
+
},
|
|
879
868
|
/**
|
|
880
|
-
*
|
|
869
|
+
* Create an error JSON-RPC response
|
|
881
870
|
*/
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
871
|
+
error(code, message, data, id = 1) {
|
|
872
|
+
return {
|
|
873
|
+
jsonrpc: "2.0",
|
|
874
|
+
id,
|
|
875
|
+
error: { code, message, data }
|
|
876
|
+
};
|
|
877
|
+
},
|
|
886
878
|
/**
|
|
887
|
-
*
|
|
879
|
+
* Create a tool result response
|
|
888
880
|
*/
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
881
|
+
toolResult(content, id = 1) {
|
|
882
|
+
return {
|
|
883
|
+
jsonrpc: "2.0",
|
|
884
|
+
id,
|
|
885
|
+
result: { content }
|
|
886
|
+
};
|
|
887
|
+
},
|
|
893
888
|
/**
|
|
894
|
-
*
|
|
889
|
+
* Create a tools/list response
|
|
895
890
|
*/
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
891
|
+
toolsList(tools, id = 1) {
|
|
892
|
+
return {
|
|
893
|
+
jsonrpc: "2.0",
|
|
894
|
+
id,
|
|
895
|
+
result: { tools }
|
|
896
|
+
};
|
|
897
|
+
},
|
|
900
898
|
/**
|
|
901
|
-
*
|
|
902
|
-
* When true, no Authorization header is sent and anonymous token is not requested.
|
|
903
|
-
* Use this for testing public/unauthenticated endpoints in CI/CD pipelines.
|
|
899
|
+
* Create a resources/list response
|
|
904
900
|
*/
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
901
|
+
resourcesList(resources, id = 1) {
|
|
902
|
+
return {
|
|
903
|
+
jsonrpc: "2.0",
|
|
904
|
+
id,
|
|
905
|
+
result: { resources }
|
|
906
|
+
};
|
|
907
|
+
},
|
|
909
908
|
/**
|
|
910
|
-
*
|
|
909
|
+
* Create a resources/read response
|
|
911
910
|
*/
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
911
|
+
resourceRead(contents, id = 1) {
|
|
912
|
+
return {
|
|
913
|
+
jsonrpc: "2.0",
|
|
914
|
+
id,
|
|
915
|
+
result: { contents }
|
|
916
|
+
};
|
|
917
|
+
},
|
|
916
918
|
/**
|
|
917
|
-
*
|
|
919
|
+
* Common MCP errors
|
|
918
920
|
*/
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
921
|
+
errors: {
|
|
922
|
+
methodNotFound: (method, id = 1) => mockResponse.error(-32601, `Method not found: ${method}`, void 0, id),
|
|
923
|
+
invalidParams: (message, id = 1) => mockResponse.error(-32602, message, void 0, id),
|
|
924
|
+
internalError: (message, id = 1) => mockResponse.error(-32603, message, void 0, id),
|
|
925
|
+
resourceNotFound: (uri, id = 1) => mockResponse.error(-32002, `Resource not found: ${uri}`, { uri }, id),
|
|
926
|
+
toolNotFound: (name, id = 1) => mockResponse.error(-32601, `Tool not found: ${name}`, { name }, id),
|
|
927
|
+
unauthorized: (id = 1) => mockResponse.error(-32001, "Unauthorized", void 0, id),
|
|
928
|
+
forbidden: (id = 1) => mockResponse.error(-32003, "Forbidden", void 0, id)
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
// libs/testing/src/interceptor/interceptor-chain.ts
|
|
933
|
+
var DefaultInterceptorChain = class {
|
|
934
|
+
request = [];
|
|
935
|
+
response = [];
|
|
936
|
+
mocks;
|
|
937
|
+
constructor() {
|
|
938
|
+
this.mocks = new DefaultMockRegistry();
|
|
922
939
|
}
|
|
923
940
|
/**
|
|
924
|
-
*
|
|
925
|
-
* Automatically configures clientInfo and capabilities for platform detection.
|
|
926
|
-
*
|
|
927
|
-
* Platform-specific behavior:
|
|
928
|
-
* - `openai`: Uses openai/* meta keys, sets User-Agent to "ChatGPT/1.0"
|
|
929
|
-
* - `ext-apps`: Uses ui/* meta keys per SEP-1865, sets io.modelcontextprotocol/ui capability
|
|
930
|
-
* - `claude`: Uses frontmcp/* + ui/* keys, sets User-Agent to "claude-desktop/1.0"
|
|
931
|
-
* - `cursor`: Uses frontmcp/* + ui/* keys, sets User-Agent to "cursor/1.0"
|
|
932
|
-
* - Other platforms follow similar patterns
|
|
933
|
-
*
|
|
934
|
-
* @example
|
|
935
|
-
* ```typescript
|
|
936
|
-
* const client = await McpTestClient.create({ baseUrl })
|
|
937
|
-
* .withPlatform('openai')
|
|
938
|
-
* .buildAndConnect();
|
|
939
|
-
*
|
|
940
|
-
* // ext-apps automatically sets the io.modelcontextprotocol/ui capability
|
|
941
|
-
* const extAppsClient = await McpTestClient.create({ baseUrl })
|
|
942
|
-
* .withPlatform('ext-apps')
|
|
943
|
-
* .buildAndConnect();
|
|
944
|
-
* ```
|
|
941
|
+
* Add a request interceptor
|
|
945
942
|
*/
|
|
946
|
-
|
|
947
|
-
this.
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
943
|
+
addRequestInterceptor(interceptor) {
|
|
944
|
+
this.request.push(interceptor);
|
|
945
|
+
return () => {
|
|
946
|
+
const index = this.request.indexOf(interceptor);
|
|
947
|
+
if (index !== -1) {
|
|
948
|
+
this.request.splice(index, 1);
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
951
|
}
|
|
952
952
|
/**
|
|
953
|
-
*
|
|
954
|
-
* Use this for fine-grained control over capabilities sent during initialization.
|
|
955
|
-
*
|
|
956
|
-
* @example
|
|
957
|
-
* ```typescript
|
|
958
|
-
* const client = await McpTestClient.create({ baseUrl })
|
|
959
|
-
* .withCapabilities({
|
|
960
|
-
* sampling: {},
|
|
961
|
-
* experimental: {
|
|
962
|
-
* 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html;profile=mcp-app'] }
|
|
963
|
-
* }
|
|
964
|
-
* })
|
|
965
|
-
* .buildAndConnect();
|
|
966
|
-
* ```
|
|
953
|
+
* Add a response interceptor
|
|
967
954
|
*/
|
|
968
|
-
|
|
969
|
-
this.
|
|
970
|
-
return
|
|
955
|
+
addResponseInterceptor(interceptor) {
|
|
956
|
+
this.response.push(interceptor);
|
|
957
|
+
return () => {
|
|
958
|
+
const index = this.response.indexOf(interceptor);
|
|
959
|
+
if (index !== -1) {
|
|
960
|
+
this.response.splice(index, 1);
|
|
961
|
+
}
|
|
962
|
+
};
|
|
971
963
|
}
|
|
972
964
|
/**
|
|
973
|
-
*
|
|
974
|
-
*
|
|
975
|
-
*
|
|
976
|
-
*
|
|
977
|
-
*
|
|
978
|
-
* const client = await McpTestClient.create({ baseUrl })
|
|
979
|
-
* .withQueryParams({ mode: 'skills_only' })
|
|
980
|
-
* .buildAndConnect();
|
|
981
|
-
* ```
|
|
965
|
+
* Process a request through the interceptor chain
|
|
966
|
+
* Returns either:
|
|
967
|
+
* - { type: 'continue', request } - continue with (possibly modified) request
|
|
968
|
+
* - { type: 'mock', response } - return mock response immediately
|
|
969
|
+
* - { type: 'error', error } - throw error
|
|
982
970
|
*/
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
971
|
+
async processRequest(request, meta) {
|
|
972
|
+
let currentRequest = request;
|
|
973
|
+
const mockDef = this.mocks.match(request);
|
|
974
|
+
if (mockDef) {
|
|
975
|
+
if (mockDef.delay && mockDef.delay > 0) {
|
|
976
|
+
await sleep3(mockDef.delay);
|
|
977
|
+
}
|
|
978
|
+
let mockResponse2;
|
|
979
|
+
if (typeof mockDef.response === "function") {
|
|
980
|
+
mockResponse2 = await mockDef.response(request);
|
|
981
|
+
} else {
|
|
982
|
+
mockResponse2 = mockDef.response;
|
|
983
|
+
}
|
|
984
|
+
return {
|
|
985
|
+
type: "mock",
|
|
986
|
+
response: { ...mockResponse2, id: request.id ?? mockResponse2.id }
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
for (const interceptor of this.request) {
|
|
990
|
+
const ctx = {
|
|
991
|
+
request: currentRequest,
|
|
992
|
+
meta
|
|
993
|
+
};
|
|
994
|
+
const result = await interceptor(ctx);
|
|
995
|
+
switch (result.action) {
|
|
996
|
+
case "passthrough":
|
|
997
|
+
break;
|
|
998
|
+
case "modify":
|
|
999
|
+
currentRequest = result.request;
|
|
1000
|
+
break;
|
|
1001
|
+
case "mock":
|
|
1002
|
+
return {
|
|
1003
|
+
type: "mock",
|
|
1004
|
+
response: { ...result.response, id: request.id ?? result.response.id }
|
|
1005
|
+
};
|
|
1006
|
+
case "error":
|
|
1007
|
+
return { type: "error", error: result.error };
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return { type: "continue", request: currentRequest };
|
|
986
1011
|
}
|
|
987
1012
|
/**
|
|
988
|
-
*
|
|
1013
|
+
* Process a response through the interceptor chain
|
|
989
1014
|
*/
|
|
990
|
-
|
|
991
|
-
|
|
1015
|
+
async processResponse(request, response, durationMs) {
|
|
1016
|
+
let currentResponse = response;
|
|
1017
|
+
for (const interceptor of this.response) {
|
|
1018
|
+
const ctx = {
|
|
1019
|
+
request,
|
|
1020
|
+
response: currentResponse,
|
|
1021
|
+
durationMs
|
|
1022
|
+
};
|
|
1023
|
+
const result = await interceptor(ctx);
|
|
1024
|
+
switch (result.action) {
|
|
1025
|
+
case "passthrough":
|
|
1026
|
+
break;
|
|
1027
|
+
case "modify":
|
|
1028
|
+
currentResponse = result.response;
|
|
1029
|
+
break;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return currentResponse;
|
|
992
1033
|
}
|
|
993
1034
|
/**
|
|
994
|
-
*
|
|
1035
|
+
* Clear all interceptors and mocks
|
|
995
1036
|
*/
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1037
|
+
clear() {
|
|
1038
|
+
this.request = [];
|
|
1039
|
+
this.response = [];
|
|
1040
|
+
this.mocks.clear();
|
|
1000
1041
|
}
|
|
1001
1042
|
};
|
|
1043
|
+
function sleep3(ms) {
|
|
1044
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1045
|
+
}
|
|
1002
1046
|
|
|
1003
1047
|
// libs/testing/src/transport/streamable-http.transport.ts
|
|
1004
1048
|
var DEFAULT_TIMEOUT = 3e4;
|
|
@@ -1017,6 +1061,7 @@ var StreamableHttpTransport = class {
|
|
|
1017
1061
|
this.config = {
|
|
1018
1062
|
baseUrl: config.baseUrl.replace(/\/$/, ""),
|
|
1019
1063
|
// Remove trailing slash
|
|
1064
|
+
entryPath: config.entryPath,
|
|
1020
1065
|
timeout: config.timeout ?? DEFAULT_TIMEOUT,
|
|
1021
1066
|
auth: config.auth ?? {},
|
|
1022
1067
|
publicMode: config.publicMode ?? false,
|
|
@@ -1109,7 +1154,7 @@ var StreamableHttpTransport = class {
|
|
|
1109
1154
|
}
|
|
1110
1155
|
const headers = this.buildHeaders();
|
|
1111
1156
|
this.lastRequestHeaders = headers;
|
|
1112
|
-
const url =
|
|
1157
|
+
const url = this.mcpUrl();
|
|
1113
1158
|
this.log(`POST ${url}`, message);
|
|
1114
1159
|
const controller = new AbortController();
|
|
1115
1160
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
@@ -1179,7 +1224,7 @@ var StreamableHttpTransport = class {
|
|
|
1179
1224
|
this.ensureConnected();
|
|
1180
1225
|
const headers = this.buildHeaders();
|
|
1181
1226
|
this.lastRequestHeaders = headers;
|
|
1182
|
-
const url =
|
|
1227
|
+
const url = this.mcpUrl();
|
|
1183
1228
|
this.log(`POST ${url} (notification)`, message);
|
|
1184
1229
|
const controller = new AbortController();
|
|
1185
1230
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
@@ -1210,7 +1255,7 @@ var StreamableHttpTransport = class {
|
|
|
1210
1255
|
this.ensureConnected();
|
|
1211
1256
|
const headers = this.buildHeaders();
|
|
1212
1257
|
this.lastRequestHeaders = headers;
|
|
1213
|
-
const url =
|
|
1258
|
+
const url = this.mcpUrl();
|
|
1214
1259
|
this.log(`POST ${url} (raw)`, data);
|
|
1215
1260
|
const controller = new AbortController();
|
|
1216
1261
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
@@ -1493,7 +1538,7 @@ var StreamableHttpTransport = class {
|
|
|
1493
1538
|
*/
|
|
1494
1539
|
async sendElicitationResponse(requestId, response) {
|
|
1495
1540
|
const headers = this.buildHeaders();
|
|
1496
|
-
const url =
|
|
1541
|
+
const url = this.mcpUrl();
|
|
1497
1542
|
const rpcResponse = {
|
|
1498
1543
|
jsonrpc: "2.0",
|
|
1499
1544
|
id: requestId,
|
|
@@ -1513,6 +1558,18 @@ var StreamableHttpTransport = class {
|
|
|
1513
1558
|
this.log("Failed to send elicitation response:", error);
|
|
1514
1559
|
}
|
|
1515
1560
|
}
|
|
1561
|
+
/**
|
|
1562
|
+
* The MCP endpoint URL. `entryPath` is where the server mounts MCP; the
|
|
1563
|
+
* trailing slash is kept because both hosts normalize it and the original
|
|
1564
|
+
* behaviour (`${baseUrl}/`) relied on it (issue #543).
|
|
1565
|
+
*/
|
|
1566
|
+
mcpUrl() {
|
|
1567
|
+
const url = new URL(this.config.baseUrl);
|
|
1568
|
+
const entry = (this.config.entryPath ?? "").replace(/^\/+|\/+$/g, "");
|
|
1569
|
+
const basePath = url.pathname.replace(/\/+$/, "");
|
|
1570
|
+
url.pathname = entry ? `${basePath}/${entry}` : `${basePath}/`;
|
|
1571
|
+
return url.toString();
|
|
1572
|
+
}
|
|
1516
1573
|
buildHeaders() {
|
|
1517
1574
|
const headers = {
|
|
1518
1575
|
"Content-Type": "application/json",
|
|
@@ -1600,277 +1657,233 @@ var StreamableHttpTransport = class {
|
|
|
1600
1657
|
}
|
|
1601
1658
|
};
|
|
1602
1659
|
|
|
1603
|
-
// libs/testing/src/
|
|
1604
|
-
var
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1660
|
+
// libs/testing/src/platform/platform-client-info.ts
|
|
1661
|
+
var MCP_APPS_EXTENSION_KEY = "io.modelcontextprotocol/ui";
|
|
1662
|
+
function getPlatformClientInfo(platform) {
|
|
1663
|
+
switch (platform) {
|
|
1664
|
+
case "openai":
|
|
1665
|
+
return {
|
|
1666
|
+
name: "ChatGPT",
|
|
1667
|
+
version: "1.0"
|
|
1668
|
+
};
|
|
1669
|
+
case "ext-apps":
|
|
1670
|
+
return {
|
|
1671
|
+
name: "mcp-ext-apps",
|
|
1672
|
+
version: "1.0"
|
|
1673
|
+
};
|
|
1674
|
+
case "claude":
|
|
1675
|
+
return {
|
|
1676
|
+
name: "claude-desktop",
|
|
1677
|
+
version: "1.0"
|
|
1678
|
+
};
|
|
1679
|
+
case "cursor":
|
|
1680
|
+
return {
|
|
1681
|
+
name: "cursor",
|
|
1682
|
+
version: "1.0"
|
|
1683
|
+
};
|
|
1684
|
+
case "continue":
|
|
1685
|
+
return {
|
|
1686
|
+
name: "continue",
|
|
1687
|
+
version: "1.0"
|
|
1688
|
+
};
|
|
1689
|
+
case "cody":
|
|
1690
|
+
return {
|
|
1691
|
+
name: "cody",
|
|
1692
|
+
version: "1.0"
|
|
1693
|
+
};
|
|
1694
|
+
case "gemini":
|
|
1695
|
+
return {
|
|
1696
|
+
name: "gemini",
|
|
1697
|
+
version: "1.0"
|
|
1698
|
+
};
|
|
1699
|
+
case "generic-mcp":
|
|
1700
|
+
return {
|
|
1701
|
+
name: "generic-mcp-client",
|
|
1702
|
+
version: "1.0"
|
|
1703
|
+
};
|
|
1704
|
+
case "unknown":
|
|
1705
|
+
default:
|
|
1706
|
+
return {
|
|
1707
|
+
name: "mcp-test-client",
|
|
1708
|
+
version: "1.0"
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
function getPlatformCapabilities(platform) {
|
|
1713
|
+
const baseCapabilities = {
|
|
1714
|
+
sampling: {},
|
|
1715
|
+
// Include elicitation.form by default for testing elicitation workflows
|
|
1716
|
+
// Note: MCP SDK expects form to be an object, not boolean
|
|
1717
|
+
elicitation: {
|
|
1718
|
+
form: {}
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1721
|
+
if (platform === "ext-apps") {
|
|
1614
1722
|
return {
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1723
|
+
...baseCapabilities,
|
|
1724
|
+
experimental: {
|
|
1725
|
+
[MCP_APPS_EXTENSION_KEY]: {
|
|
1726
|
+
mimeTypes: ["text/html;profile=mcp-app"]
|
|
1619
1727
|
}
|
|
1620
|
-
}
|
|
1621
|
-
callCount: () => entry.callCount,
|
|
1622
|
-
calls: () => [...entry.calls]
|
|
1728
|
+
}
|
|
1623
1729
|
};
|
|
1624
1730
|
}
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1731
|
+
return baseCapabilities;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// libs/testing/src/client/mcp-test-client.builder.ts
|
|
1735
|
+
var McpTestClientBuilder = class {
|
|
1736
|
+
config;
|
|
1737
|
+
constructor(config) {
|
|
1738
|
+
this.config = { ...config };
|
|
1630
1739
|
}
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
const params = request.params ?? {};
|
|
1638
|
-
if (typeof definition.params === "function") {
|
|
1639
|
-
if (!definition.params(params)) continue;
|
|
1640
|
-
} else {
|
|
1641
|
-
if (!this.paramsMatch(definition.params, params)) continue;
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
entry.callCount++;
|
|
1645
|
-
entry.calls.push(request);
|
|
1646
|
-
entry.remainingUses--;
|
|
1647
|
-
return definition;
|
|
1648
|
-
}
|
|
1649
|
-
return void 0;
|
|
1740
|
+
/**
|
|
1741
|
+
* Set the authentication configuration
|
|
1742
|
+
*/
|
|
1743
|
+
withAuth(auth) {
|
|
1744
|
+
this.config.auth = { ...this.config.auth, ...auth };
|
|
1745
|
+
return this;
|
|
1650
1746
|
}
|
|
1651
1747
|
/**
|
|
1652
|
-
*
|
|
1748
|
+
* Set the bearer token for authentication
|
|
1653
1749
|
*/
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
const actualValue = actual[key];
|
|
1658
|
-
if (Array.isArray(value)) {
|
|
1659
|
-
if (!Array.isArray(actualValue)) return false;
|
|
1660
|
-
if (value.length !== actualValue.length) return false;
|
|
1661
|
-
for (let i = 0; i < value.length; i++) {
|
|
1662
|
-
const expectedItem = value[i];
|
|
1663
|
-
const actualItem = actualValue[i];
|
|
1664
|
-
if (typeof expectedItem === "object" && expectedItem !== null) {
|
|
1665
|
-
if (typeof actualItem !== "object" || actualItem === null) return false;
|
|
1666
|
-
if (!this.paramsMatch(expectedItem, actualItem)) {
|
|
1667
|
-
return false;
|
|
1668
|
-
}
|
|
1669
|
-
} else if (actualItem !== expectedItem) {
|
|
1670
|
-
return false;
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
} else if (typeof value === "object" && value !== null) {
|
|
1674
|
-
if (typeof actualValue !== "object" || actualValue === null) return false;
|
|
1675
|
-
if (!this.paramsMatch(value, actualValue)) {
|
|
1676
|
-
return false;
|
|
1677
|
-
}
|
|
1678
|
-
} else if (actualValue !== value) {
|
|
1679
|
-
return false;
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
return true;
|
|
1750
|
+
withToken(token) {
|
|
1751
|
+
this.config.auth = { ...this.config.auth, token };
|
|
1752
|
+
return this;
|
|
1683
1753
|
}
|
|
1684
|
-
};
|
|
1685
|
-
var mockResponse = {
|
|
1686
1754
|
/**
|
|
1687
|
-
*
|
|
1755
|
+
* Add custom headers to all requests
|
|
1688
1756
|
*/
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
result
|
|
1757
|
+
withHeaders(headers) {
|
|
1758
|
+
this.config.auth = {
|
|
1759
|
+
...this.config.auth,
|
|
1760
|
+
headers: { ...this.config.auth?.headers, ...headers }
|
|
1694
1761
|
};
|
|
1695
|
-
|
|
1762
|
+
return this;
|
|
1763
|
+
}
|
|
1696
1764
|
/**
|
|
1697
|
-
*
|
|
1765
|
+
* Set the transport type
|
|
1698
1766
|
*/
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
error: { code, message, data }
|
|
1704
|
-
};
|
|
1705
|
-
},
|
|
1767
|
+
withTransport(transport) {
|
|
1768
|
+
this.config.transport = transport;
|
|
1769
|
+
return this;
|
|
1770
|
+
}
|
|
1706
1771
|
/**
|
|
1707
|
-
*
|
|
1772
|
+
* Set the request timeout in milliseconds
|
|
1708
1773
|
*/
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
result: { content }
|
|
1714
|
-
};
|
|
1715
|
-
},
|
|
1774
|
+
withTimeout(timeoutMs) {
|
|
1775
|
+
this.config.timeout = timeoutMs;
|
|
1776
|
+
return this;
|
|
1777
|
+
}
|
|
1716
1778
|
/**
|
|
1717
|
-
*
|
|
1779
|
+
* Enable debug logging
|
|
1718
1780
|
*/
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
result: { tools }
|
|
1724
|
-
};
|
|
1725
|
-
},
|
|
1781
|
+
withDebug(enabled = true) {
|
|
1782
|
+
this.config.debug = enabled;
|
|
1783
|
+
return this;
|
|
1784
|
+
}
|
|
1726
1785
|
/**
|
|
1727
|
-
*
|
|
1786
|
+
* Enable public mode - skip authentication entirely.
|
|
1787
|
+
* When true, no Authorization header is sent and anonymous token is not requested.
|
|
1788
|
+
* Use this for testing public/unauthenticated endpoints in CI/CD pipelines.
|
|
1728
1789
|
*/
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
result: { resources }
|
|
1734
|
-
};
|
|
1735
|
-
},
|
|
1790
|
+
withPublicMode(enabled = true) {
|
|
1791
|
+
this.config.publicMode = enabled;
|
|
1792
|
+
return this;
|
|
1793
|
+
}
|
|
1736
1794
|
/**
|
|
1737
|
-
*
|
|
1795
|
+
* Set the MCP protocol version to request
|
|
1738
1796
|
*/
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
result: { contents }
|
|
1744
|
-
};
|
|
1745
|
-
},
|
|
1797
|
+
withProtocolVersion(version) {
|
|
1798
|
+
this.config.protocolVersion = version;
|
|
1799
|
+
return this;
|
|
1800
|
+
}
|
|
1746
1801
|
/**
|
|
1747
|
-
*
|
|
1802
|
+
* Set the client info sent during initialization
|
|
1748
1803
|
*/
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
internalError: (message, id = 1) => mockResponse.error(-32603, message, void 0, id),
|
|
1753
|
-
resourceNotFound: (uri, id = 1) => mockResponse.error(-32002, `Resource not found: ${uri}`, { uri }, id),
|
|
1754
|
-
toolNotFound: (name, id = 1) => mockResponse.error(-32601, `Tool not found: ${name}`, { name }, id),
|
|
1755
|
-
unauthorized: (id = 1) => mockResponse.error(-32001, "Unauthorized", void 0, id),
|
|
1756
|
-
forbidden: (id = 1) => mockResponse.error(-32003, "Forbidden", void 0, id)
|
|
1757
|
-
}
|
|
1758
|
-
};
|
|
1759
|
-
|
|
1760
|
-
// libs/testing/src/interceptor/interceptor-chain.ts
|
|
1761
|
-
var DefaultInterceptorChain = class {
|
|
1762
|
-
request = [];
|
|
1763
|
-
response = [];
|
|
1764
|
-
mocks;
|
|
1765
|
-
constructor() {
|
|
1766
|
-
this.mocks = new DefaultMockRegistry();
|
|
1804
|
+
withClientInfo(info) {
|
|
1805
|
+
this.config.clientInfo = info;
|
|
1806
|
+
return this;
|
|
1767
1807
|
}
|
|
1768
1808
|
/**
|
|
1769
|
-
*
|
|
1809
|
+
* Set the platform type for testing platform-specific meta keys.
|
|
1810
|
+
* Automatically configures clientInfo and capabilities for platform detection.
|
|
1811
|
+
*
|
|
1812
|
+
* Platform-specific behavior:
|
|
1813
|
+
* - `openai`: Uses openai/* meta keys, sets User-Agent to "ChatGPT/1.0"
|
|
1814
|
+
* - `ext-apps`: Uses ui/* meta keys per SEP-1865, sets io.modelcontextprotocol/ui capability
|
|
1815
|
+
* - `claude`: Uses frontmcp/* + ui/* keys, sets User-Agent to "claude-desktop/1.0"
|
|
1816
|
+
* - `cursor`: Uses frontmcp/* + ui/* keys, sets User-Agent to "cursor/1.0"
|
|
1817
|
+
* - Other platforms follow similar patterns
|
|
1818
|
+
*
|
|
1819
|
+
* @example
|
|
1820
|
+
* ```typescript
|
|
1821
|
+
* const client = await McpTestClient.create({ baseUrl })
|
|
1822
|
+
* .withPlatform('openai')
|
|
1823
|
+
* .buildAndConnect();
|
|
1824
|
+
*
|
|
1825
|
+
* // ext-apps automatically sets the io.modelcontextprotocol/ui capability
|
|
1826
|
+
* const extAppsClient = await McpTestClient.create({ baseUrl })
|
|
1827
|
+
* .withPlatform('ext-apps')
|
|
1828
|
+
* .buildAndConnect();
|
|
1829
|
+
* ```
|
|
1770
1830
|
*/
|
|
1771
|
-
|
|
1772
|
-
this.
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
this.request.splice(index, 1);
|
|
1777
|
-
}
|
|
1778
|
-
};
|
|
1831
|
+
withPlatform(platform) {
|
|
1832
|
+
this.config.platform = platform;
|
|
1833
|
+
this.config.clientInfo = getPlatformClientInfo(platform);
|
|
1834
|
+
this.config.capabilities = getPlatformCapabilities(platform);
|
|
1835
|
+
return this;
|
|
1779
1836
|
}
|
|
1780
1837
|
/**
|
|
1781
|
-
*
|
|
1838
|
+
* Set custom client capabilities for MCP initialization.
|
|
1839
|
+
* Use this for fine-grained control over capabilities sent during initialization.
|
|
1840
|
+
*
|
|
1841
|
+
* @example
|
|
1842
|
+
* ```typescript
|
|
1843
|
+
* const client = await McpTestClient.create({ baseUrl })
|
|
1844
|
+
* .withCapabilities({
|
|
1845
|
+
* sampling: {},
|
|
1846
|
+
* experimental: {
|
|
1847
|
+
* 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html;profile=mcp-app'] }
|
|
1848
|
+
* }
|
|
1849
|
+
* })
|
|
1850
|
+
* .buildAndConnect();
|
|
1851
|
+
* ```
|
|
1782
1852
|
*/
|
|
1783
|
-
|
|
1784
|
-
this.
|
|
1785
|
-
return
|
|
1786
|
-
const index = this.response.indexOf(interceptor);
|
|
1787
|
-
if (index !== -1) {
|
|
1788
|
-
this.response.splice(index, 1);
|
|
1789
|
-
}
|
|
1790
|
-
};
|
|
1853
|
+
withCapabilities(capabilities) {
|
|
1854
|
+
this.config.capabilities = capabilities;
|
|
1855
|
+
return this;
|
|
1791
1856
|
}
|
|
1792
1857
|
/**
|
|
1793
|
-
*
|
|
1794
|
-
*
|
|
1795
|
-
*
|
|
1796
|
-
*
|
|
1797
|
-
*
|
|
1858
|
+
* Set query parameters to append to the connection URL.
|
|
1859
|
+
* Useful for testing mode switches like `?mode=skills_only`.
|
|
1860
|
+
*
|
|
1861
|
+
* @example
|
|
1862
|
+
* ```typescript
|
|
1863
|
+
* const client = await McpTestClient.create({ baseUrl })
|
|
1864
|
+
* .withQueryParams({ mode: 'skills_only' })
|
|
1865
|
+
* .buildAndConnect();
|
|
1866
|
+
* ```
|
|
1798
1867
|
*/
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
if (mockDef) {
|
|
1803
|
-
if (mockDef.delay && mockDef.delay > 0) {
|
|
1804
|
-
await sleep3(mockDef.delay);
|
|
1805
|
-
}
|
|
1806
|
-
let mockResponse2;
|
|
1807
|
-
if (typeof mockDef.response === "function") {
|
|
1808
|
-
mockResponse2 = await mockDef.response(request);
|
|
1809
|
-
} else {
|
|
1810
|
-
mockResponse2 = mockDef.response;
|
|
1811
|
-
}
|
|
1812
|
-
return {
|
|
1813
|
-
type: "mock",
|
|
1814
|
-
response: { ...mockResponse2, id: request.id ?? mockResponse2.id }
|
|
1815
|
-
};
|
|
1816
|
-
}
|
|
1817
|
-
for (const interceptor of this.request) {
|
|
1818
|
-
const ctx = {
|
|
1819
|
-
request: currentRequest,
|
|
1820
|
-
meta
|
|
1821
|
-
};
|
|
1822
|
-
const result = await interceptor(ctx);
|
|
1823
|
-
switch (result.action) {
|
|
1824
|
-
case "passthrough":
|
|
1825
|
-
break;
|
|
1826
|
-
case "modify":
|
|
1827
|
-
currentRequest = result.request;
|
|
1828
|
-
break;
|
|
1829
|
-
case "mock":
|
|
1830
|
-
return {
|
|
1831
|
-
type: "mock",
|
|
1832
|
-
response: { ...result.response, id: request.id ?? result.response.id }
|
|
1833
|
-
};
|
|
1834
|
-
case "error":
|
|
1835
|
-
return { type: "error", error: result.error };
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
|
-
return { type: "continue", request: currentRequest };
|
|
1868
|
+
withQueryParams(params) {
|
|
1869
|
+
this.config.queryParams = { ...this.config.queryParams, ...params };
|
|
1870
|
+
return this;
|
|
1839
1871
|
}
|
|
1840
1872
|
/**
|
|
1841
|
-
*
|
|
1873
|
+
* Build the McpTestClient instance (does not connect)
|
|
1842
1874
|
*/
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
for (const interceptor of this.response) {
|
|
1846
|
-
const ctx = {
|
|
1847
|
-
request,
|
|
1848
|
-
response: currentResponse,
|
|
1849
|
-
durationMs
|
|
1850
|
-
};
|
|
1851
|
-
const result = await interceptor(ctx);
|
|
1852
|
-
switch (result.action) {
|
|
1853
|
-
case "passthrough":
|
|
1854
|
-
break;
|
|
1855
|
-
case "modify":
|
|
1856
|
-
currentResponse = result.response;
|
|
1857
|
-
break;
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
return currentResponse;
|
|
1875
|
+
build() {
|
|
1876
|
+
return new McpTestClient(this.config);
|
|
1861
1877
|
}
|
|
1862
1878
|
/**
|
|
1863
|
-
*
|
|
1879
|
+
* Build the McpTestClient and connect to the server
|
|
1864
1880
|
*/
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1881
|
+
async buildAndConnect() {
|
|
1882
|
+
const client = this.build();
|
|
1883
|
+
await client.connect();
|
|
1884
|
+
return client;
|
|
1869
1885
|
}
|
|
1870
1886
|
};
|
|
1871
|
-
function sleep3(ms) {
|
|
1872
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1873
|
-
}
|
|
1874
1887
|
|
|
1875
1888
|
// libs/testing/src/client/mcp-test-client.ts
|
|
1876
1889
|
var DEFAULT_TIMEOUT2 = 3e4;
|
|
@@ -1879,6 +1892,21 @@ var DEFAULT_CLIENT_INFO = {
|
|
|
1879
1892
|
name: "@frontmcp/testing",
|
|
1880
1893
|
version: "0.4.0"
|
|
1881
1894
|
};
|
|
1895
|
+
function extractEntryPaths(data) {
|
|
1896
|
+
let payload = data;
|
|
1897
|
+
if (typeof payload === "string") {
|
|
1898
|
+
try {
|
|
1899
|
+
payload = JSON.parse(payload);
|
|
1900
|
+
} catch {
|
|
1901
|
+
return void 0;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
if (!payload || typeof payload !== "object") return void 0;
|
|
1905
|
+
const paths = payload.entryPaths;
|
|
1906
|
+
if (!Array.isArray(paths)) return void 0;
|
|
1907
|
+
const strings = paths.filter((p) => typeof p === "string");
|
|
1908
|
+
return strings.length > 0 ? strings : void 0;
|
|
1909
|
+
}
|
|
1882
1910
|
var McpTestClient = class {
|
|
1883
1911
|
// Platform, capabilities, and queryParams are optional - only set when needed
|
|
1884
1912
|
config;
|
|
@@ -1887,6 +1915,8 @@ var McpTestClient = class {
|
|
|
1887
1915
|
requestIdCounter = 0;
|
|
1888
1916
|
_lastRequestId = 0;
|
|
1889
1917
|
_sessionId;
|
|
1918
|
+
/** #543 — discovery is attempted at most once per connect. */
|
|
1919
|
+
triedDiscoveredEntryPath = false;
|
|
1890
1920
|
_sessionInfo = null;
|
|
1891
1921
|
_authState = { isAnonymous: true, scopes: [] };
|
|
1892
1922
|
// Logging and tracing
|
|
@@ -1904,6 +1934,7 @@ var McpTestClient = class {
|
|
|
1904
1934
|
constructor(config) {
|
|
1905
1935
|
this.config = {
|
|
1906
1936
|
baseUrl: config.baseUrl,
|
|
1937
|
+
entryPath: config.entryPath,
|
|
1907
1938
|
transport: config.transport ?? "streamable-http",
|
|
1908
1939
|
auth: config.auth ?? {},
|
|
1909
1940
|
publicMode: config.publicMode ?? false,
|
|
@@ -1939,11 +1970,13 @@ var McpTestClient = class {
|
|
|
1939
1970
|
*/
|
|
1940
1971
|
async connect() {
|
|
1941
1972
|
this.log("debug", `Connecting to ${this.config.baseUrl}...`);
|
|
1973
|
+
this.triedDiscoveredEntryPath = false;
|
|
1942
1974
|
this.transport = this.createTransport();
|
|
1943
1975
|
await this.transport.connect();
|
|
1944
1976
|
const maxRetries = 3;
|
|
1945
1977
|
const retryDelayMs = 500;
|
|
1946
1978
|
let lastError;
|
|
1979
|
+
let reportedEntryPaths;
|
|
1947
1980
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1948
1981
|
const initResponse = await this.initialize();
|
|
1949
1982
|
if (initResponse.success && initResponse.data) {
|
|
@@ -1951,13 +1984,27 @@ var McpTestClient = class {
|
|
|
1951
1984
|
break;
|
|
1952
1985
|
}
|
|
1953
1986
|
lastError = initResponse.error?.message ?? "Unknown error";
|
|
1987
|
+
reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
|
|
1988
|
+
if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
|
|
1989
|
+
this.triedDiscoveredEntryPath = true;
|
|
1990
|
+
const discovered = reportedEntryPaths[0];
|
|
1991
|
+
this.log("info", `Server serves MCP at ${discovered}; reconnecting there (configured: ${this.mcpPathLabel()})`);
|
|
1992
|
+
this.config.entryPath = discovered;
|
|
1993
|
+
await this.transport.close();
|
|
1994
|
+
this.transport = this.createTransport();
|
|
1995
|
+
await this.transport.connect();
|
|
1996
|
+
continue;
|
|
1997
|
+
}
|
|
1954
1998
|
if (attempt < maxRetries) {
|
|
1955
1999
|
this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
|
|
1956
2000
|
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
1957
2001
|
}
|
|
1958
2002
|
}
|
|
1959
2003
|
if (!this.initResult) {
|
|
1960
|
-
|
|
2004
|
+
const served = reportedEntryPaths?.length ? ` The server reports it serves MCP at ${reportedEntryPaths.join(", ")}; set \`entryPath\` in test.use() (or \`http.entryPath\` on the server) to match.` : "";
|
|
2005
|
+
throw new Error(
|
|
2006
|
+
`Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${maxRetries} attempts: ${lastError}.${served}`
|
|
2007
|
+
);
|
|
1961
2008
|
}
|
|
1962
2009
|
this._sessionId = this.transport.getSessionId();
|
|
1963
2010
|
this._sessionInfo = {
|
|
@@ -2555,6 +2602,11 @@ var McpTestClient = class {
|
|
|
2555
2602
|
// ═══════════════════════════════════════════════════════════════════
|
|
2556
2603
|
// PRIVATE: TRANSPORT & REQUEST HELPERS
|
|
2557
2604
|
// ═══════════════════════════════════════════════════════════════════
|
|
2605
|
+
/** The MCP path this client is currently pointed at, for log and error text. */
|
|
2606
|
+
mcpPathLabel() {
|
|
2607
|
+
const entry = (this.config.entryPath ?? "").replace(/^\/+|\/+$/g, "");
|
|
2608
|
+
return entry ? `/${entry}` : "/";
|
|
2609
|
+
}
|
|
2558
2610
|
createTransport() {
|
|
2559
2611
|
let baseUrl = this.config.baseUrl;
|
|
2560
2612
|
if (this.config.queryParams && Object.keys(this.config.queryParams).length > 0) {
|
|
@@ -2568,6 +2620,7 @@ var McpTestClient = class {
|
|
|
2568
2620
|
case "streamable-http":
|
|
2569
2621
|
return new StreamableHttpTransport({
|
|
2570
2622
|
baseUrl,
|
|
2623
|
+
entryPath: this.config.entryPath,
|
|
2571
2624
|
timeout: this.config.timeout,
|
|
2572
2625
|
auth: this.config.auth,
|
|
2573
2626
|
publicMode: this.config.publicMode,
|