@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/perf/index.js
CHANGED
|
@@ -834,233 +834,277 @@ function getMeasurementsForProject(project) {
|
|
|
834
834
|
return getGlobalMeasurementsArray().filter((m) => m.project === project);
|
|
835
835
|
}
|
|
836
836
|
|
|
837
|
-
// libs/testing/src/
|
|
838
|
-
var
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
name: "mcp-ext-apps",
|
|
849
|
-
version: "1.0"
|
|
850
|
-
};
|
|
851
|
-
case "claude":
|
|
852
|
-
return {
|
|
853
|
-
name: "claude-desktop",
|
|
854
|
-
version: "1.0"
|
|
855
|
-
};
|
|
856
|
-
case "cursor":
|
|
857
|
-
return {
|
|
858
|
-
name: "cursor",
|
|
859
|
-
version: "1.0"
|
|
860
|
-
};
|
|
861
|
-
case "continue":
|
|
862
|
-
return {
|
|
863
|
-
name: "continue",
|
|
864
|
-
version: "1.0"
|
|
865
|
-
};
|
|
866
|
-
case "cody":
|
|
867
|
-
return {
|
|
868
|
-
name: "cody",
|
|
869
|
-
version: "1.0"
|
|
870
|
-
};
|
|
871
|
-
case "gemini":
|
|
872
|
-
return {
|
|
873
|
-
name: "gemini",
|
|
874
|
-
version: "1.0"
|
|
875
|
-
};
|
|
876
|
-
case "generic-mcp":
|
|
877
|
-
return {
|
|
878
|
-
name: "generic-mcp-client",
|
|
879
|
-
version: "1.0"
|
|
880
|
-
};
|
|
881
|
-
case "unknown":
|
|
882
|
-
default:
|
|
883
|
-
return {
|
|
884
|
-
name: "mcp-test-client",
|
|
885
|
-
version: "1.0"
|
|
886
|
-
};
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
function getPlatformCapabilities(platform) {
|
|
890
|
-
const baseCapabilities = {
|
|
891
|
-
sampling: {},
|
|
892
|
-
// Include elicitation.form by default for testing elicitation workflows
|
|
893
|
-
// Note: MCP SDK expects form to be an object, not boolean
|
|
894
|
-
elicitation: {
|
|
895
|
-
form: {}
|
|
896
|
-
}
|
|
897
|
-
};
|
|
898
|
-
if (platform === "ext-apps") {
|
|
837
|
+
// libs/testing/src/interceptor/mock-registry.ts
|
|
838
|
+
var DefaultMockRegistry = class {
|
|
839
|
+
mocks = [];
|
|
840
|
+
add(mock) {
|
|
841
|
+
const entry = {
|
|
842
|
+
definition: mock,
|
|
843
|
+
callCount: 0,
|
|
844
|
+
calls: [],
|
|
845
|
+
remainingUses: mock.times ?? Infinity
|
|
846
|
+
};
|
|
847
|
+
this.mocks.push(entry);
|
|
899
848
|
return {
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
849
|
+
remove: () => {
|
|
850
|
+
const index = this.mocks.indexOf(entry);
|
|
851
|
+
if (index !== -1) {
|
|
852
|
+
this.mocks.splice(index, 1);
|
|
904
853
|
}
|
|
905
|
-
}
|
|
854
|
+
},
|
|
855
|
+
callCount: () => entry.callCount,
|
|
856
|
+
calls: () => [...entry.calls]
|
|
906
857
|
};
|
|
907
858
|
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
// libs/testing/src/client/mcp-test-client.builder.ts
|
|
912
|
-
var McpTestClientBuilder = class {
|
|
913
|
-
config;
|
|
914
|
-
constructor(config) {
|
|
915
|
-
this.config = { ...config };
|
|
859
|
+
clear() {
|
|
860
|
+
this.mocks = [];
|
|
916
861
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
862
|
+
getAll() {
|
|
863
|
+
return this.mocks.map((e) => e.definition);
|
|
864
|
+
}
|
|
865
|
+
match(request) {
|
|
866
|
+
for (const entry of this.mocks) {
|
|
867
|
+
if (entry.remainingUses <= 0) continue;
|
|
868
|
+
const { definition } = entry;
|
|
869
|
+
if (definition.method !== request.method) continue;
|
|
870
|
+
if (definition.params !== void 0) {
|
|
871
|
+
const params = request.params ?? {};
|
|
872
|
+
if (typeof definition.params === "function") {
|
|
873
|
+
if (!definition.params(params)) continue;
|
|
874
|
+
} else {
|
|
875
|
+
if (!this.paramsMatch(definition.params, params)) continue;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
entry.callCount++;
|
|
879
|
+
entry.calls.push(request);
|
|
880
|
+
entry.remainingUses--;
|
|
881
|
+
return definition;
|
|
882
|
+
}
|
|
883
|
+
return void 0;
|
|
923
884
|
}
|
|
924
885
|
/**
|
|
925
|
-
*
|
|
886
|
+
* Check if request params match the mock params definition
|
|
926
887
|
*/
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
888
|
+
paramsMatch(expected, actual) {
|
|
889
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
890
|
+
if (!(key in actual)) return false;
|
|
891
|
+
const actualValue = actual[key];
|
|
892
|
+
if (Array.isArray(value)) {
|
|
893
|
+
if (!Array.isArray(actualValue)) return false;
|
|
894
|
+
if (value.length !== actualValue.length) return false;
|
|
895
|
+
for (let i = 0; i < value.length; i++) {
|
|
896
|
+
const expectedItem = value[i];
|
|
897
|
+
const actualItem = actualValue[i];
|
|
898
|
+
if (typeof expectedItem === "object" && expectedItem !== null) {
|
|
899
|
+
if (typeof actualItem !== "object" || actualItem === null) return false;
|
|
900
|
+
if (!this.paramsMatch(expectedItem, actualItem)) {
|
|
901
|
+
return false;
|
|
902
|
+
}
|
|
903
|
+
} else if (actualItem !== expectedItem) {
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
} else if (typeof value === "object" && value !== null) {
|
|
908
|
+
if (typeof actualValue !== "object" || actualValue === null) return false;
|
|
909
|
+
if (!this.paramsMatch(value, actualValue)) {
|
|
910
|
+
return false;
|
|
911
|
+
}
|
|
912
|
+
} else if (actualValue !== value) {
|
|
913
|
+
return false;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return true;
|
|
930
917
|
}
|
|
918
|
+
};
|
|
919
|
+
var mockResponse = {
|
|
931
920
|
/**
|
|
932
|
-
*
|
|
921
|
+
* Create a successful JSON-RPC response
|
|
933
922
|
*/
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
923
|
+
success(result, id = 1) {
|
|
924
|
+
return {
|
|
925
|
+
jsonrpc: "2.0",
|
|
926
|
+
id,
|
|
927
|
+
result
|
|
938
928
|
};
|
|
939
|
-
|
|
940
|
-
}
|
|
929
|
+
},
|
|
941
930
|
/**
|
|
942
|
-
*
|
|
931
|
+
* Create an error JSON-RPC response
|
|
943
932
|
*/
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
933
|
+
error(code, message, data, id = 1) {
|
|
934
|
+
return {
|
|
935
|
+
jsonrpc: "2.0",
|
|
936
|
+
id,
|
|
937
|
+
error: { code, message, data }
|
|
938
|
+
};
|
|
939
|
+
},
|
|
948
940
|
/**
|
|
949
|
-
*
|
|
941
|
+
* Create a tool result response
|
|
950
942
|
*/
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
943
|
+
toolResult(content, id = 1) {
|
|
944
|
+
return {
|
|
945
|
+
jsonrpc: "2.0",
|
|
946
|
+
id,
|
|
947
|
+
result: { content }
|
|
948
|
+
};
|
|
949
|
+
},
|
|
955
950
|
/**
|
|
956
|
-
*
|
|
951
|
+
* Create a tools/list response
|
|
957
952
|
*/
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
953
|
+
toolsList(tools, id = 1) {
|
|
954
|
+
return {
|
|
955
|
+
jsonrpc: "2.0",
|
|
956
|
+
id,
|
|
957
|
+
result: { tools }
|
|
958
|
+
};
|
|
959
|
+
},
|
|
962
960
|
/**
|
|
963
|
-
*
|
|
964
|
-
* When true, no Authorization header is sent and anonymous token is not requested.
|
|
965
|
-
* Use this for testing public/unauthenticated endpoints in CI/CD pipelines.
|
|
961
|
+
* Create a resources/list response
|
|
966
962
|
*/
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
963
|
+
resourcesList(resources, id = 1) {
|
|
964
|
+
return {
|
|
965
|
+
jsonrpc: "2.0",
|
|
966
|
+
id,
|
|
967
|
+
result: { resources }
|
|
968
|
+
};
|
|
969
|
+
},
|
|
971
970
|
/**
|
|
972
|
-
*
|
|
971
|
+
* Create a resources/read response
|
|
973
972
|
*/
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
973
|
+
resourceRead(contents, id = 1) {
|
|
974
|
+
return {
|
|
975
|
+
jsonrpc: "2.0",
|
|
976
|
+
id,
|
|
977
|
+
result: { contents }
|
|
978
|
+
};
|
|
979
|
+
},
|
|
978
980
|
/**
|
|
979
|
-
*
|
|
981
|
+
* Common MCP errors
|
|
980
982
|
*/
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
983
|
+
errors: {
|
|
984
|
+
methodNotFound: (method, id = 1) => mockResponse.error(-32601, `Method not found: ${method}`, void 0, id),
|
|
985
|
+
invalidParams: (message, id = 1) => mockResponse.error(-32602, message, void 0, id),
|
|
986
|
+
internalError: (message, id = 1) => mockResponse.error(-32603, message, void 0, id),
|
|
987
|
+
resourceNotFound: (uri, id = 1) => mockResponse.error(-32002, `Resource not found: ${uri}`, { uri }, id),
|
|
988
|
+
toolNotFound: (name, id = 1) => mockResponse.error(-32601, `Tool not found: ${name}`, { name }, id),
|
|
989
|
+
unauthorized: (id = 1) => mockResponse.error(-32001, "Unauthorized", void 0, id),
|
|
990
|
+
forbidden: (id = 1) => mockResponse.error(-32003, "Forbidden", void 0, id)
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
// libs/testing/src/interceptor/interceptor-chain.ts
|
|
995
|
+
var DefaultInterceptorChain = class {
|
|
996
|
+
request = [];
|
|
997
|
+
response = [];
|
|
998
|
+
mocks;
|
|
999
|
+
constructor() {
|
|
1000
|
+
this.mocks = new DefaultMockRegistry();
|
|
984
1001
|
}
|
|
985
1002
|
/**
|
|
986
|
-
*
|
|
987
|
-
* Automatically configures clientInfo and capabilities for platform detection.
|
|
988
|
-
*
|
|
989
|
-
* Platform-specific behavior:
|
|
990
|
-
* - `openai`: Uses openai/* meta keys, sets User-Agent to "ChatGPT/1.0"
|
|
991
|
-
* - `ext-apps`: Uses ui/* meta keys per SEP-1865, sets io.modelcontextprotocol/ui capability
|
|
992
|
-
* - `claude`: Uses frontmcp/* + ui/* keys, sets User-Agent to "claude-desktop/1.0"
|
|
993
|
-
* - `cursor`: Uses frontmcp/* + ui/* keys, sets User-Agent to "cursor/1.0"
|
|
994
|
-
* - Other platforms follow similar patterns
|
|
995
|
-
*
|
|
996
|
-
* @example
|
|
997
|
-
* ```typescript
|
|
998
|
-
* const client = await McpTestClient.create({ baseUrl })
|
|
999
|
-
* .withPlatform('openai')
|
|
1000
|
-
* .buildAndConnect();
|
|
1001
|
-
*
|
|
1002
|
-
* // ext-apps automatically sets the io.modelcontextprotocol/ui capability
|
|
1003
|
-
* const extAppsClient = await McpTestClient.create({ baseUrl })
|
|
1004
|
-
* .withPlatform('ext-apps')
|
|
1005
|
-
* .buildAndConnect();
|
|
1006
|
-
* ```
|
|
1003
|
+
* Add a request interceptor
|
|
1007
1004
|
*/
|
|
1008
|
-
|
|
1009
|
-
this.
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1005
|
+
addRequestInterceptor(interceptor) {
|
|
1006
|
+
this.request.push(interceptor);
|
|
1007
|
+
return () => {
|
|
1008
|
+
const index = this.request.indexOf(interceptor);
|
|
1009
|
+
if (index !== -1) {
|
|
1010
|
+
this.request.splice(index, 1);
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
1013
|
}
|
|
1014
1014
|
/**
|
|
1015
|
-
*
|
|
1016
|
-
* Use this for fine-grained control over capabilities sent during initialization.
|
|
1017
|
-
*
|
|
1018
|
-
* @example
|
|
1019
|
-
* ```typescript
|
|
1020
|
-
* const client = await McpTestClient.create({ baseUrl })
|
|
1021
|
-
* .withCapabilities({
|
|
1022
|
-
* sampling: {},
|
|
1023
|
-
* experimental: {
|
|
1024
|
-
* 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html;profile=mcp-app'] }
|
|
1025
|
-
* }
|
|
1026
|
-
* })
|
|
1027
|
-
* .buildAndConnect();
|
|
1028
|
-
* ```
|
|
1015
|
+
* Add a response interceptor
|
|
1029
1016
|
*/
|
|
1030
|
-
|
|
1031
|
-
this.
|
|
1032
|
-
return
|
|
1017
|
+
addResponseInterceptor(interceptor) {
|
|
1018
|
+
this.response.push(interceptor);
|
|
1019
|
+
return () => {
|
|
1020
|
+
const index = this.response.indexOf(interceptor);
|
|
1021
|
+
if (index !== -1) {
|
|
1022
|
+
this.response.splice(index, 1);
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1033
1025
|
}
|
|
1034
1026
|
/**
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
1037
|
-
*
|
|
1038
|
-
*
|
|
1039
|
-
*
|
|
1040
|
-
* const client = await McpTestClient.create({ baseUrl })
|
|
1041
|
-
* .withQueryParams({ mode: 'skills_only' })
|
|
1042
|
-
* .buildAndConnect();
|
|
1043
|
-
* ```
|
|
1027
|
+
* Process a request through the interceptor chain
|
|
1028
|
+
* Returns either:
|
|
1029
|
+
* - { type: 'continue', request } - continue with (possibly modified) request
|
|
1030
|
+
* - { type: 'mock', response } - return mock response immediately
|
|
1031
|
+
* - { type: 'error', error } - throw error
|
|
1044
1032
|
*/
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1033
|
+
async processRequest(request, meta) {
|
|
1034
|
+
let currentRequest = request;
|
|
1035
|
+
const mockDef = this.mocks.match(request);
|
|
1036
|
+
if (mockDef) {
|
|
1037
|
+
if (mockDef.delay && mockDef.delay > 0) {
|
|
1038
|
+
await sleep3(mockDef.delay);
|
|
1039
|
+
}
|
|
1040
|
+
let mockResponse2;
|
|
1041
|
+
if (typeof mockDef.response === "function") {
|
|
1042
|
+
mockResponse2 = await mockDef.response(request);
|
|
1043
|
+
} else {
|
|
1044
|
+
mockResponse2 = mockDef.response;
|
|
1045
|
+
}
|
|
1046
|
+
return {
|
|
1047
|
+
type: "mock",
|
|
1048
|
+
response: { ...mockResponse2, id: request.id ?? mockResponse2.id }
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
for (const interceptor of this.request) {
|
|
1052
|
+
const ctx = {
|
|
1053
|
+
request: currentRequest,
|
|
1054
|
+
meta
|
|
1055
|
+
};
|
|
1056
|
+
const result = await interceptor(ctx);
|
|
1057
|
+
switch (result.action) {
|
|
1058
|
+
case "passthrough":
|
|
1059
|
+
break;
|
|
1060
|
+
case "modify":
|
|
1061
|
+
currentRequest = result.request;
|
|
1062
|
+
break;
|
|
1063
|
+
case "mock":
|
|
1064
|
+
return {
|
|
1065
|
+
type: "mock",
|
|
1066
|
+
response: { ...result.response, id: request.id ?? result.response.id }
|
|
1067
|
+
};
|
|
1068
|
+
case "error":
|
|
1069
|
+
return { type: "error", error: result.error };
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return { type: "continue", request: currentRequest };
|
|
1048
1073
|
}
|
|
1049
1074
|
/**
|
|
1050
|
-
*
|
|
1075
|
+
* Process a response through the interceptor chain
|
|
1051
1076
|
*/
|
|
1052
|
-
|
|
1053
|
-
|
|
1077
|
+
async processResponse(request, response, durationMs) {
|
|
1078
|
+
let currentResponse = response;
|
|
1079
|
+
for (const interceptor of this.response) {
|
|
1080
|
+
const ctx = {
|
|
1081
|
+
request,
|
|
1082
|
+
response: currentResponse,
|
|
1083
|
+
durationMs
|
|
1084
|
+
};
|
|
1085
|
+
const result = await interceptor(ctx);
|
|
1086
|
+
switch (result.action) {
|
|
1087
|
+
case "passthrough":
|
|
1088
|
+
break;
|
|
1089
|
+
case "modify":
|
|
1090
|
+
currentResponse = result.response;
|
|
1091
|
+
break;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return currentResponse;
|
|
1054
1095
|
}
|
|
1055
1096
|
/**
|
|
1056
|
-
*
|
|
1097
|
+
* Clear all interceptors and mocks
|
|
1057
1098
|
*/
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1099
|
+
clear() {
|
|
1100
|
+
this.request = [];
|
|
1101
|
+
this.response = [];
|
|
1102
|
+
this.mocks.clear();
|
|
1062
1103
|
}
|
|
1063
1104
|
};
|
|
1105
|
+
function sleep3(ms) {
|
|
1106
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1107
|
+
}
|
|
1064
1108
|
|
|
1065
1109
|
// libs/testing/src/transport/streamable-http.transport.ts
|
|
1066
1110
|
var DEFAULT_TIMEOUT = 3e4;
|
|
@@ -1079,6 +1123,7 @@ var StreamableHttpTransport = class {
|
|
|
1079
1123
|
this.config = {
|
|
1080
1124
|
baseUrl: config.baseUrl.replace(/\/$/, ""),
|
|
1081
1125
|
// Remove trailing slash
|
|
1126
|
+
entryPath: config.entryPath,
|
|
1082
1127
|
timeout: config.timeout ?? DEFAULT_TIMEOUT,
|
|
1083
1128
|
auth: config.auth ?? {},
|
|
1084
1129
|
publicMode: config.publicMode ?? false,
|
|
@@ -1171,7 +1216,7 @@ var StreamableHttpTransport = class {
|
|
|
1171
1216
|
}
|
|
1172
1217
|
const headers = this.buildHeaders();
|
|
1173
1218
|
this.lastRequestHeaders = headers;
|
|
1174
|
-
const url =
|
|
1219
|
+
const url = this.mcpUrl();
|
|
1175
1220
|
this.log(`POST ${url}`, message);
|
|
1176
1221
|
const controller = new AbortController();
|
|
1177
1222
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
@@ -1241,7 +1286,7 @@ var StreamableHttpTransport = class {
|
|
|
1241
1286
|
this.ensureConnected();
|
|
1242
1287
|
const headers = this.buildHeaders();
|
|
1243
1288
|
this.lastRequestHeaders = headers;
|
|
1244
|
-
const url =
|
|
1289
|
+
const url = this.mcpUrl();
|
|
1245
1290
|
this.log(`POST ${url} (notification)`, message);
|
|
1246
1291
|
const controller = new AbortController();
|
|
1247
1292
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
@@ -1272,7 +1317,7 @@ var StreamableHttpTransport = class {
|
|
|
1272
1317
|
this.ensureConnected();
|
|
1273
1318
|
const headers = this.buildHeaders();
|
|
1274
1319
|
this.lastRequestHeaders = headers;
|
|
1275
|
-
const url =
|
|
1320
|
+
const url = this.mcpUrl();
|
|
1276
1321
|
this.log(`POST ${url} (raw)`, data);
|
|
1277
1322
|
const controller = new AbortController();
|
|
1278
1323
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
@@ -1555,7 +1600,7 @@ var StreamableHttpTransport = class {
|
|
|
1555
1600
|
*/
|
|
1556
1601
|
async sendElicitationResponse(requestId, response) {
|
|
1557
1602
|
const headers = this.buildHeaders();
|
|
1558
|
-
const url =
|
|
1603
|
+
const url = this.mcpUrl();
|
|
1559
1604
|
const rpcResponse = {
|
|
1560
1605
|
jsonrpc: "2.0",
|
|
1561
1606
|
id: requestId,
|
|
@@ -1575,6 +1620,18 @@ var StreamableHttpTransport = class {
|
|
|
1575
1620
|
this.log("Failed to send elicitation response:", error);
|
|
1576
1621
|
}
|
|
1577
1622
|
}
|
|
1623
|
+
/**
|
|
1624
|
+
* The MCP endpoint URL. `entryPath` is where the server mounts MCP; the
|
|
1625
|
+
* trailing slash is kept because both hosts normalize it and the original
|
|
1626
|
+
* behaviour (`${baseUrl}/`) relied on it (issue #543).
|
|
1627
|
+
*/
|
|
1628
|
+
mcpUrl() {
|
|
1629
|
+
const url = new URL(this.config.baseUrl);
|
|
1630
|
+
const entry = (this.config.entryPath ?? "").replace(/^\/+|\/+$/g, "");
|
|
1631
|
+
const basePath = url.pathname.replace(/\/+$/, "");
|
|
1632
|
+
url.pathname = entry ? `${basePath}/${entry}` : `${basePath}/`;
|
|
1633
|
+
return url.toString();
|
|
1634
|
+
}
|
|
1578
1635
|
buildHeaders() {
|
|
1579
1636
|
const headers = {
|
|
1580
1637
|
"Content-Type": "application/json",
|
|
@@ -1662,277 +1719,233 @@ var StreamableHttpTransport = class {
|
|
|
1662
1719
|
}
|
|
1663
1720
|
};
|
|
1664
1721
|
|
|
1665
|
-
// libs/testing/src/
|
|
1666
|
-
var
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1722
|
+
// libs/testing/src/platform/platform-client-info.ts
|
|
1723
|
+
var MCP_APPS_EXTENSION_KEY = "io.modelcontextprotocol/ui";
|
|
1724
|
+
function getPlatformClientInfo(platform) {
|
|
1725
|
+
switch (platform) {
|
|
1726
|
+
case "openai":
|
|
1727
|
+
return {
|
|
1728
|
+
name: "ChatGPT",
|
|
1729
|
+
version: "1.0"
|
|
1730
|
+
};
|
|
1731
|
+
case "ext-apps":
|
|
1732
|
+
return {
|
|
1733
|
+
name: "mcp-ext-apps",
|
|
1734
|
+
version: "1.0"
|
|
1735
|
+
};
|
|
1736
|
+
case "claude":
|
|
1737
|
+
return {
|
|
1738
|
+
name: "claude-desktop",
|
|
1739
|
+
version: "1.0"
|
|
1740
|
+
};
|
|
1741
|
+
case "cursor":
|
|
1742
|
+
return {
|
|
1743
|
+
name: "cursor",
|
|
1744
|
+
version: "1.0"
|
|
1745
|
+
};
|
|
1746
|
+
case "continue":
|
|
1747
|
+
return {
|
|
1748
|
+
name: "continue",
|
|
1749
|
+
version: "1.0"
|
|
1750
|
+
};
|
|
1751
|
+
case "cody":
|
|
1752
|
+
return {
|
|
1753
|
+
name: "cody",
|
|
1754
|
+
version: "1.0"
|
|
1755
|
+
};
|
|
1756
|
+
case "gemini":
|
|
1757
|
+
return {
|
|
1758
|
+
name: "gemini",
|
|
1759
|
+
version: "1.0"
|
|
1760
|
+
};
|
|
1761
|
+
case "generic-mcp":
|
|
1762
|
+
return {
|
|
1763
|
+
name: "generic-mcp-client",
|
|
1764
|
+
version: "1.0"
|
|
1765
|
+
};
|
|
1766
|
+
case "unknown":
|
|
1767
|
+
default:
|
|
1768
|
+
return {
|
|
1769
|
+
name: "mcp-test-client",
|
|
1770
|
+
version: "1.0"
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
function getPlatformCapabilities(platform) {
|
|
1775
|
+
const baseCapabilities = {
|
|
1776
|
+
sampling: {},
|
|
1777
|
+
// Include elicitation.form by default for testing elicitation workflows
|
|
1778
|
+
// Note: MCP SDK expects form to be an object, not boolean
|
|
1779
|
+
elicitation: {
|
|
1780
|
+
form: {}
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
if (platform === "ext-apps") {
|
|
1676
1784
|
return {
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1785
|
+
...baseCapabilities,
|
|
1786
|
+
experimental: {
|
|
1787
|
+
[MCP_APPS_EXTENSION_KEY]: {
|
|
1788
|
+
mimeTypes: ["text/html;profile=mcp-app"]
|
|
1681
1789
|
}
|
|
1682
|
-
}
|
|
1683
|
-
callCount: () => entry.callCount,
|
|
1684
|
-
calls: () => [...entry.calls]
|
|
1790
|
+
}
|
|
1685
1791
|
};
|
|
1686
1792
|
}
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1793
|
+
return baseCapabilities;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
// libs/testing/src/client/mcp-test-client.builder.ts
|
|
1797
|
+
var McpTestClientBuilder = class {
|
|
1798
|
+
config;
|
|
1799
|
+
constructor(config) {
|
|
1800
|
+
this.config = { ...config };
|
|
1692
1801
|
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
const params = request.params ?? {};
|
|
1700
|
-
if (typeof definition.params === "function") {
|
|
1701
|
-
if (!definition.params(params)) continue;
|
|
1702
|
-
} else {
|
|
1703
|
-
if (!this.paramsMatch(definition.params, params)) continue;
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
entry.callCount++;
|
|
1707
|
-
entry.calls.push(request);
|
|
1708
|
-
entry.remainingUses--;
|
|
1709
|
-
return definition;
|
|
1710
|
-
}
|
|
1711
|
-
return void 0;
|
|
1802
|
+
/**
|
|
1803
|
+
* Set the authentication configuration
|
|
1804
|
+
*/
|
|
1805
|
+
withAuth(auth) {
|
|
1806
|
+
this.config.auth = { ...this.config.auth, ...auth };
|
|
1807
|
+
return this;
|
|
1712
1808
|
}
|
|
1713
1809
|
/**
|
|
1714
|
-
*
|
|
1810
|
+
* Set the bearer token for authentication
|
|
1715
1811
|
*/
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
const actualValue = actual[key];
|
|
1720
|
-
if (Array.isArray(value)) {
|
|
1721
|
-
if (!Array.isArray(actualValue)) return false;
|
|
1722
|
-
if (value.length !== actualValue.length) return false;
|
|
1723
|
-
for (let i = 0; i < value.length; i++) {
|
|
1724
|
-
const expectedItem = value[i];
|
|
1725
|
-
const actualItem = actualValue[i];
|
|
1726
|
-
if (typeof expectedItem === "object" && expectedItem !== null) {
|
|
1727
|
-
if (typeof actualItem !== "object" || actualItem === null) return false;
|
|
1728
|
-
if (!this.paramsMatch(expectedItem, actualItem)) {
|
|
1729
|
-
return false;
|
|
1730
|
-
}
|
|
1731
|
-
} else if (actualItem !== expectedItem) {
|
|
1732
|
-
return false;
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
} else if (typeof value === "object" && value !== null) {
|
|
1736
|
-
if (typeof actualValue !== "object" || actualValue === null) return false;
|
|
1737
|
-
if (!this.paramsMatch(value, actualValue)) {
|
|
1738
|
-
return false;
|
|
1739
|
-
}
|
|
1740
|
-
} else if (actualValue !== value) {
|
|
1741
|
-
return false;
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
return true;
|
|
1812
|
+
withToken(token) {
|
|
1813
|
+
this.config.auth = { ...this.config.auth, token };
|
|
1814
|
+
return this;
|
|
1745
1815
|
}
|
|
1746
|
-
};
|
|
1747
|
-
var mockResponse = {
|
|
1748
1816
|
/**
|
|
1749
|
-
*
|
|
1817
|
+
* Add custom headers to all requests
|
|
1750
1818
|
*/
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
result
|
|
1819
|
+
withHeaders(headers) {
|
|
1820
|
+
this.config.auth = {
|
|
1821
|
+
...this.config.auth,
|
|
1822
|
+
headers: { ...this.config.auth?.headers, ...headers }
|
|
1756
1823
|
};
|
|
1757
|
-
|
|
1824
|
+
return this;
|
|
1825
|
+
}
|
|
1758
1826
|
/**
|
|
1759
|
-
*
|
|
1827
|
+
* Set the transport type
|
|
1760
1828
|
*/
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
error: { code, message, data }
|
|
1766
|
-
};
|
|
1767
|
-
},
|
|
1829
|
+
withTransport(transport) {
|
|
1830
|
+
this.config.transport = transport;
|
|
1831
|
+
return this;
|
|
1832
|
+
}
|
|
1768
1833
|
/**
|
|
1769
|
-
*
|
|
1834
|
+
* Set the request timeout in milliseconds
|
|
1770
1835
|
*/
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
result: { content }
|
|
1776
|
-
};
|
|
1777
|
-
},
|
|
1836
|
+
withTimeout(timeoutMs) {
|
|
1837
|
+
this.config.timeout = timeoutMs;
|
|
1838
|
+
return this;
|
|
1839
|
+
}
|
|
1778
1840
|
/**
|
|
1779
|
-
*
|
|
1841
|
+
* Enable debug logging
|
|
1780
1842
|
*/
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
result: { tools }
|
|
1786
|
-
};
|
|
1787
|
-
},
|
|
1843
|
+
withDebug(enabled = true) {
|
|
1844
|
+
this.config.debug = enabled;
|
|
1845
|
+
return this;
|
|
1846
|
+
}
|
|
1788
1847
|
/**
|
|
1789
|
-
*
|
|
1848
|
+
* Enable public mode - skip authentication entirely.
|
|
1849
|
+
* When true, no Authorization header is sent and anonymous token is not requested.
|
|
1850
|
+
* Use this for testing public/unauthenticated endpoints in CI/CD pipelines.
|
|
1790
1851
|
*/
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
result: { resources }
|
|
1796
|
-
};
|
|
1797
|
-
},
|
|
1852
|
+
withPublicMode(enabled = true) {
|
|
1853
|
+
this.config.publicMode = enabled;
|
|
1854
|
+
return this;
|
|
1855
|
+
}
|
|
1798
1856
|
/**
|
|
1799
|
-
*
|
|
1857
|
+
* Set the MCP protocol version to request
|
|
1800
1858
|
*/
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
result: { contents }
|
|
1806
|
-
};
|
|
1807
|
-
},
|
|
1859
|
+
withProtocolVersion(version) {
|
|
1860
|
+
this.config.protocolVersion = version;
|
|
1861
|
+
return this;
|
|
1862
|
+
}
|
|
1808
1863
|
/**
|
|
1809
|
-
*
|
|
1864
|
+
* Set the client info sent during initialization
|
|
1810
1865
|
*/
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
internalError: (message, id = 1) => mockResponse.error(-32603, message, void 0, id),
|
|
1815
|
-
resourceNotFound: (uri, id = 1) => mockResponse.error(-32002, `Resource not found: ${uri}`, { uri }, id),
|
|
1816
|
-
toolNotFound: (name, id = 1) => mockResponse.error(-32601, `Tool not found: ${name}`, { name }, id),
|
|
1817
|
-
unauthorized: (id = 1) => mockResponse.error(-32001, "Unauthorized", void 0, id),
|
|
1818
|
-
forbidden: (id = 1) => mockResponse.error(-32003, "Forbidden", void 0, id)
|
|
1819
|
-
}
|
|
1820
|
-
};
|
|
1821
|
-
|
|
1822
|
-
// libs/testing/src/interceptor/interceptor-chain.ts
|
|
1823
|
-
var DefaultInterceptorChain = class {
|
|
1824
|
-
request = [];
|
|
1825
|
-
response = [];
|
|
1826
|
-
mocks;
|
|
1827
|
-
constructor() {
|
|
1828
|
-
this.mocks = new DefaultMockRegistry();
|
|
1866
|
+
withClientInfo(info) {
|
|
1867
|
+
this.config.clientInfo = info;
|
|
1868
|
+
return this;
|
|
1829
1869
|
}
|
|
1830
1870
|
/**
|
|
1831
|
-
*
|
|
1871
|
+
* Set the platform type for testing platform-specific meta keys.
|
|
1872
|
+
* Automatically configures clientInfo and capabilities for platform detection.
|
|
1873
|
+
*
|
|
1874
|
+
* Platform-specific behavior:
|
|
1875
|
+
* - `openai`: Uses openai/* meta keys, sets User-Agent to "ChatGPT/1.0"
|
|
1876
|
+
* - `ext-apps`: Uses ui/* meta keys per SEP-1865, sets io.modelcontextprotocol/ui capability
|
|
1877
|
+
* - `claude`: Uses frontmcp/* + ui/* keys, sets User-Agent to "claude-desktop/1.0"
|
|
1878
|
+
* - `cursor`: Uses frontmcp/* + ui/* keys, sets User-Agent to "cursor/1.0"
|
|
1879
|
+
* - Other platforms follow similar patterns
|
|
1880
|
+
*
|
|
1881
|
+
* @example
|
|
1882
|
+
* ```typescript
|
|
1883
|
+
* const client = await McpTestClient.create({ baseUrl })
|
|
1884
|
+
* .withPlatform('openai')
|
|
1885
|
+
* .buildAndConnect();
|
|
1886
|
+
*
|
|
1887
|
+
* // ext-apps automatically sets the io.modelcontextprotocol/ui capability
|
|
1888
|
+
* const extAppsClient = await McpTestClient.create({ baseUrl })
|
|
1889
|
+
* .withPlatform('ext-apps')
|
|
1890
|
+
* .buildAndConnect();
|
|
1891
|
+
* ```
|
|
1832
1892
|
*/
|
|
1833
|
-
|
|
1834
|
-
this.
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
this.request.splice(index, 1);
|
|
1839
|
-
}
|
|
1840
|
-
};
|
|
1893
|
+
withPlatform(platform) {
|
|
1894
|
+
this.config.platform = platform;
|
|
1895
|
+
this.config.clientInfo = getPlatformClientInfo(platform);
|
|
1896
|
+
this.config.capabilities = getPlatformCapabilities(platform);
|
|
1897
|
+
return this;
|
|
1841
1898
|
}
|
|
1842
1899
|
/**
|
|
1843
|
-
*
|
|
1900
|
+
* Set custom client capabilities for MCP initialization.
|
|
1901
|
+
* Use this for fine-grained control over capabilities sent during initialization.
|
|
1902
|
+
*
|
|
1903
|
+
* @example
|
|
1904
|
+
* ```typescript
|
|
1905
|
+
* const client = await McpTestClient.create({ baseUrl })
|
|
1906
|
+
* .withCapabilities({
|
|
1907
|
+
* sampling: {},
|
|
1908
|
+
* experimental: {
|
|
1909
|
+
* 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html;profile=mcp-app'] }
|
|
1910
|
+
* }
|
|
1911
|
+
* })
|
|
1912
|
+
* .buildAndConnect();
|
|
1913
|
+
* ```
|
|
1844
1914
|
*/
|
|
1845
|
-
|
|
1846
|
-
this.
|
|
1847
|
-
return
|
|
1848
|
-
const index = this.response.indexOf(interceptor);
|
|
1849
|
-
if (index !== -1) {
|
|
1850
|
-
this.response.splice(index, 1);
|
|
1851
|
-
}
|
|
1852
|
-
};
|
|
1915
|
+
withCapabilities(capabilities) {
|
|
1916
|
+
this.config.capabilities = capabilities;
|
|
1917
|
+
return this;
|
|
1853
1918
|
}
|
|
1854
1919
|
/**
|
|
1855
|
-
*
|
|
1856
|
-
*
|
|
1857
|
-
*
|
|
1858
|
-
*
|
|
1859
|
-
*
|
|
1920
|
+
* Set query parameters to append to the connection URL.
|
|
1921
|
+
* Useful for testing mode switches like `?mode=skills_only`.
|
|
1922
|
+
*
|
|
1923
|
+
* @example
|
|
1924
|
+
* ```typescript
|
|
1925
|
+
* const client = await McpTestClient.create({ baseUrl })
|
|
1926
|
+
* .withQueryParams({ mode: 'skills_only' })
|
|
1927
|
+
* .buildAndConnect();
|
|
1928
|
+
* ```
|
|
1860
1929
|
*/
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
if (mockDef) {
|
|
1865
|
-
if (mockDef.delay && mockDef.delay > 0) {
|
|
1866
|
-
await sleep3(mockDef.delay);
|
|
1867
|
-
}
|
|
1868
|
-
let mockResponse2;
|
|
1869
|
-
if (typeof mockDef.response === "function") {
|
|
1870
|
-
mockResponse2 = await mockDef.response(request);
|
|
1871
|
-
} else {
|
|
1872
|
-
mockResponse2 = mockDef.response;
|
|
1873
|
-
}
|
|
1874
|
-
return {
|
|
1875
|
-
type: "mock",
|
|
1876
|
-
response: { ...mockResponse2, id: request.id ?? mockResponse2.id }
|
|
1877
|
-
};
|
|
1878
|
-
}
|
|
1879
|
-
for (const interceptor of this.request) {
|
|
1880
|
-
const ctx = {
|
|
1881
|
-
request: currentRequest,
|
|
1882
|
-
meta
|
|
1883
|
-
};
|
|
1884
|
-
const result = await interceptor(ctx);
|
|
1885
|
-
switch (result.action) {
|
|
1886
|
-
case "passthrough":
|
|
1887
|
-
break;
|
|
1888
|
-
case "modify":
|
|
1889
|
-
currentRequest = result.request;
|
|
1890
|
-
break;
|
|
1891
|
-
case "mock":
|
|
1892
|
-
return {
|
|
1893
|
-
type: "mock",
|
|
1894
|
-
response: { ...result.response, id: request.id ?? result.response.id }
|
|
1895
|
-
};
|
|
1896
|
-
case "error":
|
|
1897
|
-
return { type: "error", error: result.error };
|
|
1898
|
-
}
|
|
1899
|
-
}
|
|
1900
|
-
return { type: "continue", request: currentRequest };
|
|
1930
|
+
withQueryParams(params) {
|
|
1931
|
+
this.config.queryParams = { ...this.config.queryParams, ...params };
|
|
1932
|
+
return this;
|
|
1901
1933
|
}
|
|
1902
1934
|
/**
|
|
1903
|
-
*
|
|
1935
|
+
* Build the McpTestClient instance (does not connect)
|
|
1904
1936
|
*/
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
for (const interceptor of this.response) {
|
|
1908
|
-
const ctx = {
|
|
1909
|
-
request,
|
|
1910
|
-
response: currentResponse,
|
|
1911
|
-
durationMs
|
|
1912
|
-
};
|
|
1913
|
-
const result = await interceptor(ctx);
|
|
1914
|
-
switch (result.action) {
|
|
1915
|
-
case "passthrough":
|
|
1916
|
-
break;
|
|
1917
|
-
case "modify":
|
|
1918
|
-
currentResponse = result.response;
|
|
1919
|
-
break;
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
return currentResponse;
|
|
1937
|
+
build() {
|
|
1938
|
+
return new McpTestClient(this.config);
|
|
1923
1939
|
}
|
|
1924
1940
|
/**
|
|
1925
|
-
*
|
|
1941
|
+
* Build the McpTestClient and connect to the server
|
|
1926
1942
|
*/
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1943
|
+
async buildAndConnect() {
|
|
1944
|
+
const client = this.build();
|
|
1945
|
+
await client.connect();
|
|
1946
|
+
return client;
|
|
1931
1947
|
}
|
|
1932
1948
|
};
|
|
1933
|
-
function sleep3(ms) {
|
|
1934
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1935
|
-
}
|
|
1936
1949
|
|
|
1937
1950
|
// libs/testing/src/client/mcp-test-client.ts
|
|
1938
1951
|
var DEFAULT_TIMEOUT2 = 3e4;
|
|
@@ -1941,6 +1954,21 @@ var DEFAULT_CLIENT_INFO = {
|
|
|
1941
1954
|
name: "@frontmcp/testing",
|
|
1942
1955
|
version: "0.4.0"
|
|
1943
1956
|
};
|
|
1957
|
+
function extractEntryPaths(data) {
|
|
1958
|
+
let payload = data;
|
|
1959
|
+
if (typeof payload === "string") {
|
|
1960
|
+
try {
|
|
1961
|
+
payload = JSON.parse(payload);
|
|
1962
|
+
} catch {
|
|
1963
|
+
return void 0;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
if (!payload || typeof payload !== "object") return void 0;
|
|
1967
|
+
const paths = payload.entryPaths;
|
|
1968
|
+
if (!Array.isArray(paths)) return void 0;
|
|
1969
|
+
const strings = paths.filter((p) => typeof p === "string");
|
|
1970
|
+
return strings.length > 0 ? strings : void 0;
|
|
1971
|
+
}
|
|
1944
1972
|
var McpTestClient = class {
|
|
1945
1973
|
// Platform, capabilities, and queryParams are optional - only set when needed
|
|
1946
1974
|
config;
|
|
@@ -1949,6 +1977,8 @@ var McpTestClient = class {
|
|
|
1949
1977
|
requestIdCounter = 0;
|
|
1950
1978
|
_lastRequestId = 0;
|
|
1951
1979
|
_sessionId;
|
|
1980
|
+
/** #543 — discovery is attempted at most once per connect. */
|
|
1981
|
+
triedDiscoveredEntryPath = false;
|
|
1952
1982
|
_sessionInfo = null;
|
|
1953
1983
|
_authState = { isAnonymous: true, scopes: [] };
|
|
1954
1984
|
// Logging and tracing
|
|
@@ -1966,6 +1996,7 @@ var McpTestClient = class {
|
|
|
1966
1996
|
constructor(config) {
|
|
1967
1997
|
this.config = {
|
|
1968
1998
|
baseUrl: config.baseUrl,
|
|
1999
|
+
entryPath: config.entryPath,
|
|
1969
2000
|
transport: config.transport ?? "streamable-http",
|
|
1970
2001
|
auth: config.auth ?? {},
|
|
1971
2002
|
publicMode: config.publicMode ?? false,
|
|
@@ -2001,11 +2032,13 @@ var McpTestClient = class {
|
|
|
2001
2032
|
*/
|
|
2002
2033
|
async connect() {
|
|
2003
2034
|
this.log("debug", `Connecting to ${this.config.baseUrl}...`);
|
|
2035
|
+
this.triedDiscoveredEntryPath = false;
|
|
2004
2036
|
this.transport = this.createTransport();
|
|
2005
2037
|
await this.transport.connect();
|
|
2006
2038
|
const maxRetries = 3;
|
|
2007
2039
|
const retryDelayMs = 500;
|
|
2008
2040
|
let lastError;
|
|
2041
|
+
let reportedEntryPaths;
|
|
2009
2042
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
2010
2043
|
const initResponse = await this.initialize();
|
|
2011
2044
|
if (initResponse.success && initResponse.data) {
|
|
@@ -2013,13 +2046,27 @@ var McpTestClient = class {
|
|
|
2013
2046
|
break;
|
|
2014
2047
|
}
|
|
2015
2048
|
lastError = initResponse.error?.message ?? "Unknown error";
|
|
2049
|
+
reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
|
|
2050
|
+
if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
|
|
2051
|
+
this.triedDiscoveredEntryPath = true;
|
|
2052
|
+
const discovered = reportedEntryPaths[0];
|
|
2053
|
+
this.log("info", `Server serves MCP at ${discovered}; reconnecting there (configured: ${this.mcpPathLabel()})`);
|
|
2054
|
+
this.config.entryPath = discovered;
|
|
2055
|
+
await this.transport.close();
|
|
2056
|
+
this.transport = this.createTransport();
|
|
2057
|
+
await this.transport.connect();
|
|
2058
|
+
continue;
|
|
2059
|
+
}
|
|
2016
2060
|
if (attempt < maxRetries) {
|
|
2017
2061
|
this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
|
|
2018
2062
|
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
2019
2063
|
}
|
|
2020
2064
|
}
|
|
2021
2065
|
if (!this.initResult) {
|
|
2022
|
-
|
|
2066
|
+
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.` : "";
|
|
2067
|
+
throw new Error(
|
|
2068
|
+
`Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${maxRetries} attempts: ${lastError}.${served}`
|
|
2069
|
+
);
|
|
2023
2070
|
}
|
|
2024
2071
|
this._sessionId = this.transport.getSessionId();
|
|
2025
2072
|
this._sessionInfo = {
|
|
@@ -2617,6 +2664,11 @@ var McpTestClient = class {
|
|
|
2617
2664
|
// ═══════════════════════════════════════════════════════════════════
|
|
2618
2665
|
// PRIVATE: TRANSPORT & REQUEST HELPERS
|
|
2619
2666
|
// ═══════════════════════════════════════════════════════════════════
|
|
2667
|
+
/** The MCP path this client is currently pointed at, for log and error text. */
|
|
2668
|
+
mcpPathLabel() {
|
|
2669
|
+
const entry = (this.config.entryPath ?? "").replace(/^\/+|\/+$/g, "");
|
|
2670
|
+
return entry ? `/${entry}` : "/";
|
|
2671
|
+
}
|
|
2620
2672
|
createTransport() {
|
|
2621
2673
|
let baseUrl = this.config.baseUrl;
|
|
2622
2674
|
if (this.config.queryParams && Object.keys(this.config.queryParams).length > 0) {
|
|
@@ -2630,6 +2682,7 @@ var McpTestClient = class {
|
|
|
2630
2682
|
case "streamable-http":
|
|
2631
2683
|
return new StreamableHttpTransport({
|
|
2632
2684
|
baseUrl,
|
|
2685
|
+
entryPath: this.config.entryPath,
|
|
2633
2686
|
timeout: this.config.timeout,
|
|
2634
2687
|
auth: this.config.auth,
|
|
2635
2688
|
publicMode: this.config.publicMode,
|