@mcpfn/testing 0.0.4 → 0.0.6

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/dist/index.cjs CHANGED
@@ -33,37 +33,66 @@ __export(index_exports, {
33
33
  MCPFN_HOST_PROFILES: () => MCPFN_HOST_PROFILES,
34
34
  MCPFN_NAMED_OAUTH_HOST_FIXTURES: () => MCPFN_NAMED_OAUTH_HOST_FIXTURES,
35
35
  MCPFN_OAUTH_EXTENSION_GRANTS: () => MCPFN_OAUTH_EXTENSION_GRANTS,
36
+ MCPFN_REPORT_SCHEMA_VERSION: () => MCPFN_REPORT_SCHEMA_VERSION,
37
+ MCPFN_TESTING_VERSION: () => MCPFN_TESTING_VERSION,
36
38
  McpFnAssertionError: () => McpFnAssertionError,
37
39
  McpFnAuthAssertionError: () => McpFnAuthAssertionError,
40
+ McpFnClientProfileContractCleanupError: () => McpFnClientProfileContractCleanupError,
41
+ McpFnConformanceCleanupError: () => McpFnConformanceCleanupError,
38
42
  McpFnMockOAuthAuthorizationServer: () => McpFnMockOAuthAuthorizationServer,
43
+ McpFnRedactionLimitError: () => McpFnRedactionLimitError,
44
+ McpFnStructuralCredentialCollisionError: () => McpFnStructuralCredentialCollisionError,
45
+ McpFnTargetSuiteArtifactCleanupError: () => McpFnTargetSuiteArtifactCleanupError,
46
+ McpFnTargetSuiteCleanupError: () => McpFnTargetSuiteCleanupError,
39
47
  McpFnTestClient: () => McpFnTestClient,
48
+ McpFnTestClientCleanupError: () => McpFnTestClientCleanupError,
40
49
  OFFICIAL_CONFORMANCE_VERSION: () => OFFICIAL_CONFORMANCE_VERSION,
50
+ acquireRemoteCredential: () => acquireRemoteCredential,
41
51
  apiKeyCredential: () => apiKeyCredential,
42
52
  assertAuthRegressionSuite: () => assertAuthRegressionSuite,
43
53
  assertAuthorizationCodeClientMetadata: () => assertAuthorizationCodeClientMetadata,
44
54
  assertBearerResourceChallenge: () => assertBearerResourceChallenge,
45
55
  assertManifestContract: () => assertManifestContract,
46
56
  assertStructuredTextParity: () => assertStructuredTextParity,
57
+ assertTargetArtifactSafe: () => assertTargetArtifactSafe,
58
+ authenticatedHttpTarget: () => authenticatedHttpTarget,
47
59
  bearerCredential: () => bearerCredential,
60
+ beginTargetCredentialRedaction: () => beginTargetCredentialRedaction,
48
61
  buildOfficialConformanceArgs: () => buildOfficialConformanceArgs,
62
+ buildOfficialConformanceEnvironment: () => buildOfficialConformanceEnvironment,
49
63
  checkHostCompatibility: () => checkHostCompatibility,
50
64
  createAuthenticatedConformanceProxy: () => createAuthenticatedConformanceProxy,
51
65
  createFetchAuthTarget: () => createFetchAuthTarget,
66
+ createHostedAuthorizationFixtures: () => createHostedAuthorizationFixtures,
67
+ createMcpFnClientProfileSnapshot: () => createMcpFnClientProfileSnapshot,
52
68
  createMcpFnScenario: () => createMcpFnScenario,
53
69
  createMcpFnScenarioArtifact: () => createMcpFnScenarioArtifact,
54
70
  createMcpFnScenarioReport: () => createMcpFnScenarioReport,
71
+ createMcpFnTargetSuiteJUnit: () => createMcpFnTargetSuiteJUnit,
55
72
  createOAuthClientMetadataFixture: () => createOAuthClientMetadataFixture,
56
73
  createOAuthClientMetadataVariants: () => createOAuthClientMetadataVariants,
57
74
  createPkcePair: () => createPkcePair,
75
+ diffMcpFnClientProfileSnapshots: () => diffMcpFnClientProfileSnapshots,
76
+ disposeMcpFnTargetSuiteReport: () => disposeMcpFnTargetSuiteReport,
77
+ normalizeMcpFnReportFailure: () => normalizeMcpFnReportFailure,
58
78
  parseBearerChallenge: () => parseBearerChallenge,
79
+ redactRemoteCredential: () => redactRemoteCredential,
80
+ redactTargetCredentials: () => redactTargetCredentials,
59
81
  runAuthRegressionSuite: () => runAuthRegressionSuite,
60
82
  runAuthenticatedOfficialConformance: () => runAuthenticatedOfficialConformance,
83
+ runHostedAuthorizationRegression: () => runHostedAuthorizationRegression,
84
+ runMcpFnClientProfileContracts: () => runMcpFnClientProfileContracts,
61
85
  runMcpFnTargetSuite: () => runMcpFnTargetSuite,
62
86
  runOfficialConformance: () => runOfficialConformance,
63
87
  runScenarios: () => runScenarios,
88
+ serializeMcpFnTargetSuiteReport: () => serializeMcpFnTargetSuiteReport,
64
89
  stableJson: () => stableJson,
65
90
  startMockOAuthAuthorizationServer: () => startMockOAuthAuthorizationServer,
66
- validateMcpFnScenarios: () => validateMcpFnScenarios
91
+ staticRemoteCredentialProvider: () => staticRemoteCredentialProvider,
92
+ validateMcpFnClientProfileSnapshot: () => validateMcpFnClientProfileSnapshot,
93
+ validateMcpFnScenarios: () => validateMcpFnScenarios,
94
+ validateMcpFnSchemaPortability: () => validateMcpFnSchemaPortability,
95
+ validateRemoteCredentialHeaders: () => validateRemoteCredentialHeaders
67
96
  });
68
97
  module.exports = __toCommonJS(index_exports);
69
98
 
@@ -559,7 +588,7 @@ function uniqueScopes(value) {
559
588
  function json(status, body) {
560
589
  return Response.json(body, {
561
590
  status,
562
- headers: { "cache-control": "no-store" }
591
+ headers: { "cache-control": "no-store", pragma: "no-cache" }
563
592
  });
564
593
  }
565
594
  function oauthError(status, error, description) {
@@ -664,8 +693,8 @@ var McpFnMockOAuthAuthorizationServer = class {
664
693
  }
665
694
  async handle(request) {
666
695
  const url = new URL(request.url);
667
- const path2 = url.pathname.replace(this.issuer.pathname.replace(/\/$/, ""), "") || "/";
668
- if (request.method === "GET" && path2 === "/.well-known/oauth-authorization-server") {
696
+ const path3 = url.pathname.replace(this.issuer.pathname.replace(/\/$/, ""), "") || "/";
697
+ if (request.method === "GET" && path3 === "/.well-known/oauth-authorization-server") {
669
698
  return json(200, {
670
699
  issuer: this.issuer.toString(),
671
700
  authorization_endpoint: this.authorizationEndpoint,
@@ -679,20 +708,20 @@ var McpFnMockOAuthAuthorizationServer = class {
679
708
  client_id_metadata_document_supported: true
680
709
  });
681
710
  }
682
- if (request.method === "GET" && path2.startsWith("/client-metadata/")) {
711
+ if (request.method === "GET" && path3.startsWith("/client-metadata/")) {
683
712
  const metadata = Object.values(this.clientMetadata).find((candidate) => new URL(candidate.client_id).pathname === url.pathname);
684
713
  return metadata ? json(200, metadata) : json(404, { error: "not_found" });
685
714
  }
686
- if (request.method === "GET" && path2 === "/authorize") {
715
+ if (request.method === "GET" && path3 === "/authorize") {
687
716
  return this.authorize(url);
688
717
  }
689
- if (request.method === "POST" && path2 === "/token") {
718
+ if (request.method === "POST" && path3 === "/token") {
690
719
  return this.token(await request.text());
691
720
  }
692
- if (request.method === "POST" && path2 === "/revoke") {
721
+ if (request.method === "POST" && path3 === "/revoke") {
693
722
  return this.revoke(await request.text());
694
723
  }
695
- if (request.method === "GET" && path2 === "/callback") {
724
+ if (request.method === "GET" && path3 === "/callback") {
696
725
  const parameters = Object.fromEntries(url.searchParams);
697
726
  this.callbacks.push({ url: url.toString(), parameters });
698
727
  return new Response(
@@ -903,6 +932,34 @@ async function startMockOAuthAuthorizationServer(options = {}) {
903
932
  // src/client.ts
904
933
  var import_client = require("@mcpfn/client");
905
934
  var import_inMemory = require("@modelcontextprotocol/sdk/inMemory.js");
935
+ var testClientCleanupOwners = /* @__PURE__ */ new WeakMap();
936
+ var McpFnTestClientCleanupError = class extends Error {
937
+ constructor(close, cause) {
938
+ super(
939
+ "Test client cleanup remains pending; retain this error and retryCleanup()",
940
+ { cause }
941
+ );
942
+ this.name = "McpFnTestClientCleanupError";
943
+ testClientCleanupOwners.set(this, { close });
944
+ }
945
+ retryCleanup() {
946
+ const owner = testClientCleanupOwners.get(this);
947
+ if (!owner) return Promise.resolve();
948
+ if (owner.pending) return owner.pending;
949
+ const pending = Promise.resolve().then(owner.close).then(
950
+ () => {
951
+ testClientCleanupOwners.delete(this);
952
+ },
953
+ () => {
954
+ throw this;
955
+ }
956
+ ).finally(() => {
957
+ owner.pending = void 0;
958
+ });
959
+ owner.pending = pending;
960
+ return pending;
961
+ }
962
+ };
906
963
  var McpFnTestClient = class _McpFnTestClient {
907
964
  session;
908
965
  constructor(session) {
@@ -923,8 +980,9 @@ var McpFnTestClient = class _McpFnTestClient {
923
980
  }
924
981
  }), info, options);
925
982
  }
926
- static async connectTarget(target, info, options = {}) {
927
- const session = (0, import_client.createMcpFnClient)({
983
+ /** Create the session owner before connecting, so failed initialization remains closeable. */
984
+ static createTarget(target, info, options = {}) {
985
+ return new _McpFnTestClient((0, import_client.createMcpFnClient)({
928
986
  target,
929
987
  info: info ?? { name: "mcpfn-test-client", version: "1.0.0" },
930
988
  capabilities: options.capabilities,
@@ -932,12 +990,20 @@ var McpFnTestClient = class _McpFnTestClient {
932
990
  events: options.events,
933
991
  configure: options.configure,
934
992
  diagnostics: options.diagnostics
935
- });
993
+ }));
994
+ }
995
+ static async connectTarget(target, info, options = {}) {
996
+ const client = this.createTarget(target, info, options);
936
997
  try {
937
- await session.connect();
938
- return new _McpFnTestClient(session);
998
+ await client.session.connect();
999
+ return client;
939
1000
  } catch (error) {
940
- await session.close(true).catch(() => void 0);
1001
+ const connectionFailure = error instanceof Error && error.cause !== void 0 ? error.cause : error;
1002
+ try {
1003
+ await client.session.close();
1004
+ } catch {
1005
+ throw new McpFnTestClientCleanupError(() => client.session.close(), connectionFailure);
1006
+ }
941
1007
  throw error;
942
1008
  }
943
1009
  }
@@ -986,20 +1052,1578 @@ var McpFnTestClient = class _McpFnTestClient {
986
1052
  complete(params, options) {
987
1053
  return this.session.prompts.complete(params, options);
988
1054
  }
989
- close() {
990
- return this.session.close();
1055
+ async close() {
1056
+ try {
1057
+ await this.session.close();
1058
+ } catch (error) {
1059
+ throw new McpFnTestClientCleanupError(() => this.session.close(), error);
1060
+ }
991
1061
  }
992
1062
  };
993
1063
 
994
- // src/conformance.ts
1064
+ // src/client-profile-contracts.ts
1065
+ var import_node_crypto2 = require("crypto");
1066
+ var import_ajv = __toESM(require("ajv"), 1);
1067
+ var import__ = __toESM(require("ajv/dist/2019.js"), 1);
1068
+ var import__2 = __toESM(require("ajv/dist/2020.js"), 1);
1069
+ var import_oauth_core2 = require("@superfunctions/oauth-core");
1070
+ var McpFnClientProfileContractCleanupError = class extends Error {
1071
+ constructor(report, owners) {
1072
+ super("Client profile target cleanup remains pending; retain this error and retryCleanup()");
1073
+ this.report = report;
1074
+ this.owners = owners;
1075
+ this.name = "McpFnClientProfileContractCleanupError";
1076
+ }
1077
+ report;
1078
+ owners;
1079
+ async retryCleanup() {
1080
+ const results = await Promise.allSettled(this.owners.map((owner) => owner.retryCleanup()));
1081
+ if (results.some((result) => result.status === "rejected")) throw this;
1082
+ }
1083
+ };
1084
+ var ProfileCaseCleanupError = class extends Error {
1085
+ constructor(owner, result) {
1086
+ super("Client profile target cleanup remains pending");
1087
+ this.owner = owner;
1088
+ this.result = result;
1089
+ }
1090
+ owner;
1091
+ result;
1092
+ };
1093
+ var PORTABILITY_KEYWORDS = /* @__PURE__ */ new Set([
1094
+ "$anchor",
1095
+ "$recursiveAnchor",
1096
+ "$recursiveRef",
1097
+ "$dynamicAnchor",
1098
+ "$dynamicRef",
1099
+ "dependentSchemas",
1100
+ "dependentRequired",
1101
+ "minContains",
1102
+ "maxContains",
1103
+ "prefixItems",
1104
+ "unevaluatedItems",
1105
+ "unevaluatedProperties"
1106
+ ]);
1107
+ function hash(value) {
1108
+ return (0, import_node_crypto2.createHash)("sha256").update(stableJson(value)).digest("hex");
1109
+ }
1110
+ function boundedError(value) {
1111
+ const redacted = String(
1112
+ (0, import_oauth_core2.redactOAuthValue)(value instanceof Error ? value.message : value)
1113
+ );
1114
+ return redacted.slice(0, 512);
1115
+ }
1116
+ function assertProfileReference(id, version) {
1117
+ for (const [label, value] of [
1118
+ ["id", id],
1119
+ ["version", version]
1120
+ ]) {
1121
+ if (typeof value !== "string" || !value || value.length > 128 || !/^[A-Za-z0-9._/-]+$/.test(value)) {
1122
+ throw new Error(
1123
+ `Client profile ${label} must be a non-empty stable identifier`
1124
+ );
1125
+ }
1126
+ }
1127
+ }
1128
+ function createMcpFnClientProfileSnapshot(profile, tools) {
1129
+ assertProfileReference(profile.id, profile.version);
1130
+ const normalized = [...tools].map((tool) => structuredClone(tool)).sort(
1131
+ (left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0
1132
+ );
1133
+ if (normalized.some((tool, index) => index > 0 && tool.name === normalized[index - 1].name)) {
1134
+ throw new Error("Effective catalog contains duplicate tool names");
1135
+ }
1136
+ return {
1137
+ formatVersion: 1,
1138
+ kind: "mcpfn.client-profile-snapshot",
1139
+ profile: { id: profile.id, version: profile.version },
1140
+ catalogHash: hash(normalized),
1141
+ tools: normalized.map((tool) => ({ name: tool.name, hash: hash(tool) }))
1142
+ };
1143
+ }
1144
+ function assertSnapshotFields(value, fields, location) {
1145
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1146
+ throw new Error(`Client profile snapshot ${location} must be an object`);
1147
+ }
1148
+ const key = Object.keys(value).find((key2) => !fields.includes(key2));
1149
+ if (key !== void 0) {
1150
+ throw new Error(`Client profile snapshot contains unsupported fields at ${location}: ${JSON.stringify(key.slice(0, 128))}`);
1151
+ }
1152
+ }
1153
+ function validateMcpFnClientProfileSnapshot(value) {
1154
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1155
+ throw new Error("Client profile snapshot must be an object");
1156
+ }
1157
+ assertSnapshotFields(value, ["formatVersion", "kind", "profile", "catalogHash", "tools"], "root");
1158
+ const snapshot = value;
1159
+ if (snapshot.formatVersion !== 1 || snapshot.kind !== "mcpfn.client-profile-snapshot") {
1160
+ throw new Error("Unsupported client profile snapshot format");
1161
+ }
1162
+ if (!snapshot.profile)
1163
+ throw new Error("Client profile snapshot requires profile metadata");
1164
+ assertSnapshotFields(snapshot.profile, ["id", "version"], "profile");
1165
+ assertProfileReference(snapshot.profile.id, snapshot.profile.version);
1166
+ if (typeof snapshot.catalogHash !== "string" || !/^[a-f0-9]{64}$/.test(snapshot.catalogHash)) {
1167
+ throw new Error("Client profile snapshot requires a SHA-256 catalogHash");
1168
+ }
1169
+ if (!Array.isArray(snapshot.tools))
1170
+ throw new Error("Client profile snapshot tools must be an array");
1171
+ let prior = "";
1172
+ for (const [index, tool] of snapshot.tools.entries()) {
1173
+ assertSnapshotFields(tool, ["name", "hash"], `tools[${index}]`);
1174
+ if (!tool || typeof tool.name !== "string" || !tool.name || !/^[a-f0-9]{64}$/.test(tool.hash)) {
1175
+ throw new Error("Client profile snapshot contains an invalid tool entry");
1176
+ }
1177
+ if (tool.name <= prior)
1178
+ throw new Error(
1179
+ "Client profile snapshot tools must be sorted and unique"
1180
+ );
1181
+ prior = tool.name;
1182
+ }
1183
+ return snapshot;
1184
+ }
1185
+ function diffMcpFnClientProfileSnapshots(beforeValue, afterValue) {
1186
+ const before = validateMcpFnClientProfileSnapshot(beforeValue);
1187
+ const after = validateMcpFnClientProfileSnapshot(afterValue);
1188
+ if (before.profile.id !== after.profile.id) {
1189
+ throw new Error("Cannot diff snapshots for different client profile ids");
1190
+ }
1191
+ const beforeByName = new Map(
1192
+ before.tools.map((tool) => [tool.name, tool.hash])
1193
+ );
1194
+ const afterByName = new Map(
1195
+ after.tools.map((tool) => [tool.name, tool.hash])
1196
+ );
1197
+ const names = [
1198
+ .../* @__PURE__ */ new Set([...beforeByName.keys(), ...afterByName.keys()])
1199
+ ].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
1200
+ const changes = [];
1201
+ for (const tool of names) {
1202
+ const beforeHash = beforeByName.get(tool);
1203
+ const afterHash = afterByName.get(tool);
1204
+ if (!beforeHash) changes.push({ kind: "added", tool, afterHash });
1205
+ else if (!afterHash) changes.push({ kind: "removed", tool, beforeHash });
1206
+ else if (beforeHash !== afterHash) {
1207
+ changes.push({ kind: "modified", tool, beforeHash, afterHash });
1208
+ }
1209
+ }
1210
+ if (changes.length === 0 !== (before.catalogHash === after.catalogHash)) {
1211
+ throw new Error("Inconsistent catalog hashes for tool entries");
1212
+ }
1213
+ const summary = {
1214
+ added: changes.filter(({ kind }) => kind === "added").length,
1215
+ removed: changes.filter(({ kind }) => kind === "removed").length,
1216
+ modified: changes.filter(({ kind }) => kind === "modified").length
1217
+ };
1218
+ return { compatible: summary.removed === 0, changes, summary };
1219
+ }
1220
+ function createDialectValidator(dialect) {
1221
+ const normalized = dialect.replace(/#$/, "");
1222
+ if (normalized === "http://json-schema.org/draft-07/schema" || normalized === "https://json-schema.org/draft-07/schema")
1223
+ return new import_ajv.default({ allErrors: true, strict: false, validateFormats: false });
1224
+ if (normalized === "http://json-schema.org/draft/2019-09/schema" || normalized === "https://json-schema.org/draft/2019-09/schema")
1225
+ return new import__.default({
1226
+ allErrors: true,
1227
+ strict: false,
1228
+ validateFormats: false
1229
+ });
1230
+ if (normalized === "http://json-schema.org/draft/2020-12/schema" || normalized === "https://json-schema.org/draft/2020-12/schema")
1231
+ return new import__2.default({
1232
+ allErrors: true,
1233
+ strict: false,
1234
+ validateFormats: false
1235
+ });
1236
+ return void 0;
1237
+ }
1238
+ function pointerSegment(value) {
1239
+ return value.replaceAll("~", "~0").replaceAll("/", "~1");
1240
+ }
1241
+ function walkSchema(value, path3, visit) {
1242
+ if (!value || typeof value !== "object") return;
1243
+ const schema = value;
1244
+ for (const keyword of Object.keys(schema)) {
1245
+ visit(keyword, `${path3}/${pointerSegment(keyword)}`, schema);
1246
+ }
1247
+ for (const mapKeyword of [
1248
+ "$defs",
1249
+ "definitions",
1250
+ "dependencies",
1251
+ "dependentSchemas",
1252
+ "patternProperties",
1253
+ "properties"
1254
+ ]) {
1255
+ const entries = schema[mapKeyword];
1256
+ if (!entries || typeof entries !== "object" || Array.isArray(entries))
1257
+ continue;
1258
+ for (const [name, child] of Object.entries(
1259
+ entries
1260
+ )) {
1261
+ walkSchema(child, `${path3}/${mapKeyword}/${pointerSegment(name)}`, visit);
1262
+ }
1263
+ }
1264
+ for (const arrayKeyword of ["allOf", "anyOf", "oneOf", "prefixItems", "items"]) {
1265
+ const entries = schema[arrayKeyword];
1266
+ if (!Array.isArray(entries)) continue;
1267
+ entries.forEach(
1268
+ (child, index) => walkSchema(child, `${path3}/${arrayKeyword}/${index}`, visit)
1269
+ );
1270
+ }
1271
+ for (const schemaKeyword of [
1272
+ "additionalItems",
1273
+ "additionalProperties",
1274
+ "contains",
1275
+ "contentSchema",
1276
+ "else",
1277
+ "if",
1278
+ "items",
1279
+ "not",
1280
+ "propertyNames",
1281
+ "then",
1282
+ "unevaluatedItems",
1283
+ "unevaluatedProperties"
1284
+ ]) {
1285
+ const child = schema[schemaKeyword];
1286
+ if (child && typeof child === "object" && !Array.isArray(child)) {
1287
+ walkSchema(child, `${path3}/${schemaKeyword}`, visit);
1288
+ }
1289
+ }
1290
+ }
1291
+ function validateMcpFnSchemaPortability(schema, path3, policy = {}) {
1292
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
1293
+ return [
1294
+ {
1295
+ severity: "error",
1296
+ code: "schema-invalid",
1297
+ path: path3,
1298
+ message: "Tool schema must be an object"
1299
+ }
1300
+ ];
1301
+ }
1302
+ const declared = schema.$schema;
1303
+ const dialect = typeof declared === "string" ? declared : "http://json-schema.org/draft-07/schema#";
1304
+ const validator = createDialectValidator(dialect);
1305
+ if (!validator) {
1306
+ return [
1307
+ {
1308
+ severity: "error",
1309
+ code: "schema-dialect-unsupported",
1310
+ path: `${path3}/$schema`,
1311
+ message: boundedError(`Unsupported JSON Schema dialect ${dialect}`)
1312
+ }
1313
+ ];
1314
+ }
1315
+ const issues = [];
1316
+ try {
1317
+ validator.compile(schema);
1318
+ } catch (error) {
1319
+ issues.push({
1320
+ severity: "error",
1321
+ code: "schema-invalid",
1322
+ path: path3,
1323
+ message: boundedError(error)
1324
+ });
1325
+ }
1326
+ const allowed = new Set(policy.allowKeywords ?? []);
1327
+ const modernDialect = /draft\/(2019-09|2020-12)\/schema#?$/.test(dialect);
1328
+ const assertionKeywords = /* @__PURE__ */ new Set([
1329
+ "type",
1330
+ "enum",
1331
+ "const",
1332
+ "multipleOf",
1333
+ "maximum",
1334
+ "exclusiveMaximum",
1335
+ "minimum",
1336
+ "exclusiveMinimum",
1337
+ "maxLength",
1338
+ "minLength",
1339
+ "pattern",
1340
+ "format",
1341
+ "maxItems",
1342
+ "minItems",
1343
+ "uniqueItems",
1344
+ "contains",
1345
+ "minContains",
1346
+ "maxContains",
1347
+ "maxProperties",
1348
+ "minProperties",
1349
+ "required",
1350
+ "dependentRequired",
1351
+ "properties",
1352
+ "patternProperties",
1353
+ "additionalProperties",
1354
+ "propertyNames",
1355
+ "dependentSchemas",
1356
+ "items",
1357
+ "prefixItems",
1358
+ "additionalItems",
1359
+ "unevaluatedItems",
1360
+ "unevaluatedProperties",
1361
+ "allOf",
1362
+ "anyOf",
1363
+ "oneOf",
1364
+ "not",
1365
+ "if",
1366
+ "then",
1367
+ "else"
1368
+ ]);
1369
+ walkSchema(schema, path3, (keyword, keywordPath, node) => {
1370
+ if (modernDialect && keyword === "$ref" && !allowed.has(keyword) && Object.keys(node).some((sibling) => assertionKeywords.has(sibling))) {
1371
+ issues.push({
1372
+ severity: policy.warningsAsErrors ? "error" : "warning",
1373
+ code: "schema-portability-warning",
1374
+ path: keywordPath,
1375
+ keyword,
1376
+ message: "$ref assertion siblings are evaluated by modern dialects but ignored by draft-07 clients"
1377
+ });
1378
+ }
1379
+ if (!PORTABILITY_KEYWORDS.has(keyword) || allowed.has(keyword)) return;
1380
+ issues.push({
1381
+ severity: policy.warningsAsErrors ? "error" : "warning",
1382
+ code: "schema-portability-warning",
1383
+ path: keywordPath,
1384
+ keyword,
1385
+ message: `${keyword} is valid but may reduce client portability`
1386
+ });
1387
+ });
1388
+ return issues.sort(
1389
+ (left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0
1390
+ );
1391
+ }
1392
+ function extractError(result) {
1393
+ const structured = result.structuredContent;
1394
+ if (structured?.error && typeof structured.error === "object") {
1395
+ return structured.error;
1396
+ }
1397
+ const text = result.content.find((content) => content.type === "text")?.text;
1398
+ if (!text) return void 0;
1399
+ try {
1400
+ const parsed = JSON.parse(text);
1401
+ return parsed.error && typeof parsed.error === "object" ? parsed.error : void 0;
1402
+ } catch {
1403
+ return void 0;
1404
+ }
1405
+ }
1406
+ function fixtureFailure(result, fixture) {
1407
+ const expected = fixture.expect;
1408
+ if (fixture.source !== "captured-failure" && result.isError) return "Minimal-valid fixture returned isError=true";
1409
+ if (fixture.source === "captured-failure" && !result.isError) return "Captured failure did not reproduce an error";
1410
+ if (!expected)
1411
+ return result.isError ? "Tool returned isError=true" : void 0;
1412
+ if (expected.isError !== void 0 && Boolean(result.isError) !== expected.isError) {
1413
+ return `Expected isError=${expected.isError}, received ${Boolean(result.isError)}`;
1414
+ }
1415
+ if (expected.structuredContent !== void 0 && stableJson(result.structuredContent) !== stableJson(expected.structuredContent))
1416
+ return "Structured content did not match the fixture expectation";
1417
+ const error = extractError(result);
1418
+ if (expected.errorCode !== void 0 && error?.code !== expected.errorCode) {
1419
+ return "Error code did not match the declared expectation";
1420
+ }
1421
+ const details = error?.details && typeof error.details === "object" ? error.details : void 0;
1422
+ if (expected.lifecycleStage !== void 0 && details?.lifecycleStage !== expected.lifecycleStage)
1423
+ return `Expected lifecycle stage ${expected.lifecycleStage}`;
1424
+ if (expected.validationIssue) {
1425
+ const issues = Array.isArray(details?.issues) ? details.issues : [];
1426
+ const fields = Object.entries(expected.validationIssue).filter(([, value]) => value !== void 0);
1427
+ const matched = fields.length > 0 && issues.some(
1428
+ (issue) => fields.every(
1429
+ ([key, value]) => issue[key] === value
1430
+ )
1431
+ );
1432
+ if (!matched)
1433
+ return "Expected structured validation issue was not reported";
1434
+ }
1435
+ return void 0;
1436
+ }
1437
+ async function runProfileCase(profileCase, allowSideEffects) {
1438
+ const profile = { id: profileCase.id, version: profileCase.version };
1439
+ let client;
1440
+ let configurationError;
1441
+ let pendingFailure;
1442
+ let hasPendingFailure = false;
1443
+ let cleanupOwner;
1444
+ let result = {
1445
+ profile,
1446
+ status: "complete",
1447
+ ok: false,
1448
+ portability: [],
1449
+ fixtures: []
1450
+ };
1451
+ try {
1452
+ try {
1453
+ client = await McpFnTestClient.connectTarget(
1454
+ profileCase.target,
1455
+ profileCase.clientInfo ?? {
1456
+ name: "mcpfn-profile-suite",
1457
+ version: "1.0.0"
1458
+ },
1459
+ { ...profileCase.client, capabilities: profileCase.capabilities }
1460
+ );
1461
+ } catch (error) {
1462
+ if (error instanceof McpFnTestClientCleanupError) throw error;
1463
+ return { ...result, phase: "connect", error: "Target connection failed" };
1464
+ }
1465
+ let tools;
1466
+ try {
1467
+ tools = await client.listTools();
1468
+ const repeatedTools = await client.listTools();
1469
+ if (stableJson(tools) !== stableJson(repeatedTools)) {
1470
+ throw new Error(
1471
+ "Effective catalog changed across identical profile list requests"
1472
+ );
1473
+ }
1474
+ result.snapshot = createMcpFnClientProfileSnapshot(profile, tools);
1475
+ result.snapshotMatches = profileCase.expectedSnapshot ? stableJson(result.snapshot) === stableJson(profileCase.expectedSnapshot) : void 0;
1476
+ for (const tool of tools) {
1477
+ result.portability.push(
1478
+ ...validateMcpFnSchemaPortability(
1479
+ tool.inputSchema,
1480
+ `tools/${tool.name}/inputSchema`,
1481
+ profileCase.portability
1482
+ )
1483
+ );
1484
+ if (tool.outputSchema) {
1485
+ result.portability.push(
1486
+ ...validateMcpFnSchemaPortability(
1487
+ tool.outputSchema,
1488
+ `tools/${tool.name}/outputSchema`,
1489
+ profileCase.portability
1490
+ )
1491
+ );
1492
+ }
1493
+ }
1494
+ } catch (error) {
1495
+ tools = void 0;
1496
+ result = { ...result, phase: "catalog", error: "Target catalog validation failed" };
1497
+ }
1498
+ if (tools) {
1499
+ const advertised = new Set(tools.map(({ name }) => name));
1500
+ const taskRequired = new Set(tools.filter((tool) => tool.execution?.taskSupport === "required").map((tool) => tool.name));
1501
+ if (profileCase.fixtures?.some((fixture) => taskRequired.has(fixture.tool))) {
1502
+ configurationError = new Error(`Client profile ${profile.id}@${profile.version} task-required fixtures are unsupported by this contract runner`);
1503
+ throw configurationError;
1504
+ }
1505
+ for (const fixture of profileCase.fixtures ?? []) {
1506
+ const base = {
1507
+ name: fixture.name,
1508
+ tool: fixture.tool,
1509
+ source: fixture.source ?? "minimal-valid"
1510
+ };
1511
+ if (!advertised.has(fixture.tool)) {
1512
+ result.fixtures.push({
1513
+ ...base,
1514
+ status: "failed",
1515
+ code: "fixture-tool-not-advertised",
1516
+ error: "Fixture references a tool absent from the effective catalog"
1517
+ });
1518
+ continue;
1519
+ }
1520
+ if (fixture.sideEffect !== "read-only" && !allowSideEffects) {
1521
+ result.fixtures.push({
1522
+ ...base,
1523
+ status: "incomplete",
1524
+ code: "side-effect-not-authorized",
1525
+ error: "Fixture was not executed because side effects were not explicitly allowed"
1526
+ });
1527
+ continue;
1528
+ }
1529
+ try {
1530
+ const callResult = await client.callTool(
1531
+ fixture.tool,
1532
+ fixture.arguments ?? {}
1533
+ );
1534
+ const failure = fixtureFailure(callResult, fixture);
1535
+ result.fixtures.push(
1536
+ failure ? {
1537
+ ...base,
1538
+ status: "failed",
1539
+ code: "fixture-expectation-failed",
1540
+ error: failure
1541
+ } : { ...base, status: "passed" }
1542
+ );
1543
+ } catch (error) {
1544
+ result.fixtures.push({
1545
+ ...base,
1546
+ status: "failed",
1547
+ code: "fixture-call-failed",
1548
+ error: "Tool invocation failed; argument values and exception text are omitted"
1549
+ });
1550
+ }
1551
+ }
1552
+ const incomplete = result.fixtures.some(
1553
+ ({ status }) => status === "incomplete"
1554
+ );
1555
+ const failed = result.fixtures.some(({ status }) => status === "failed") || result.portability.some(({ severity }) => severity === "error") || result.snapshotMatches === false;
1556
+ result = {
1557
+ ...result,
1558
+ phase: result.snapshotMatches === false || result.portability.some(({ severity }) => severity === "error") ? "catalog" : failed || incomplete ? "fixtures" : void 0,
1559
+ status: incomplete ? "incomplete" : "complete",
1560
+ ok: !failed && !incomplete,
1561
+ ...result.snapshotMatches === false ? {
1562
+ error: "Effective catalog does not match the reviewed profile snapshot"
1563
+ } : {}
1564
+ };
1565
+ }
1566
+ } catch (error) {
1567
+ pendingFailure = error;
1568
+ hasPendingFailure = true;
1569
+ } finally {
1570
+ if (client) {
1571
+ try {
1572
+ await client.close();
1573
+ } catch (error) {
1574
+ if (error instanceof McpFnTestClientCleanupError) cleanupOwner = error;
1575
+ else {
1576
+ if (configurationError) configurationError.message = `${configurationError.message}; target cleanup failed`;
1577
+ result = {
1578
+ ...result,
1579
+ ok: false,
1580
+ phase: result.phase ?? "close",
1581
+ error: result.error ? `${result.error}; target cleanup failed`.slice(0, 512) : "Target cleanup failed"
1582
+ };
1583
+ }
1584
+ }
1585
+ }
1586
+ }
1587
+ if (cleanupOwner) throw new ProfileCaseCleanupError(cleanupOwner, {
1588
+ ...result,
1589
+ ok: false,
1590
+ status: "incomplete",
1591
+ phase: result.phase ?? (hasPendingFailure ? "fixtures" : "close"),
1592
+ error: hasPendingFailure ? "Profile execution failed; target cleanup remains pending" : "Target cleanup remains pending"
1593
+ });
1594
+ if (hasPendingFailure) {
1595
+ if (pendingFailure instanceof McpFnTestClientCleanupError) {
1596
+ throw new ProfileCaseCleanupError(pendingFailure, {
1597
+ ...result,
1598
+ ok: false,
1599
+ status: "incomplete",
1600
+ phase: "connect",
1601
+ error: "Target cleanup remains pending"
1602
+ });
1603
+ }
1604
+ throw pendingFailure;
1605
+ }
1606
+ return result;
1607
+ }
1608
+ async function runMcpFnClientProfileContracts(options) {
1609
+ const maxReportBytes = options.maxReportBytes ?? 1048576;
1610
+ if (!Number.isSafeInteger(maxReportBytes) || maxReportBytes < 2048) {
1611
+ throw new Error("maxReportBytes must be an integer of at least 2048");
1612
+ }
1613
+ if (!Array.isArray(options.profiles) || options.profiles.length === 0) {
1614
+ throw new Error(
1615
+ "At least one generic or configured client profile is required"
1616
+ );
1617
+ }
1618
+ if (options.profiles.length > 100)
1619
+ throw new Error("At most 100 client profiles may be tested");
1620
+ const keys = /* @__PURE__ */ new Set();
1621
+ for (const profile of options.profiles) {
1622
+ assertProfileReference(profile.id, profile.version);
1623
+ const key = `${profile.id}@${profile.version}`;
1624
+ if (keys.has(key)) throw new Error(`Duplicate client profile case ${key}`);
1625
+ keys.add(key);
1626
+ if (profile.expectedSnapshot)
1627
+ validateMcpFnClientProfileSnapshot(profile.expectedSnapshot);
1628
+ for (const fixture of profile.fixtures ?? []) {
1629
+ if (fixture.source === "captured-failure" && (!fixture.expect || fixture.expect.isError === false || !(fixture.expect.structuredContent !== void 0 || fixture.expect.errorCode || fixture.expect.lifecycleStage || fixture.expect.validationIssue && Object.values(fixture.expect.validationIssue).some((value) => value !== void 0)))) {
1630
+ throw new Error(`Captured-failure fixture ${fixture.name} requires meaningful error expectations`);
1631
+ }
1632
+ if (fixture.source !== void 0 && !["minimal-valid", "captured-failure"].includes(fixture.source)) throw new Error("Invalid fixture source");
1633
+ if (!fixture.name || !fixture.tool || !["read-only", "idempotent", "non-idempotent"].includes(fixture.sideEffect)) {
1634
+ throw new Error(`Client profile ${key} contains an invalid fixture`);
1635
+ }
1636
+ }
1637
+ }
1638
+ const profiles = [];
1639
+ const cleanupOwners = [];
1640
+ for (const profile of [...options.profiles].sort((left, right) => {
1641
+ const leftKey = `${left.id}@${left.version}`;
1642
+ const rightKey = `${right.id}@${right.version}`;
1643
+ return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
1644
+ })) {
1645
+ try {
1646
+ profiles.push(await runProfileCase(profile, options.allowSideEffects === true));
1647
+ } catch (error) {
1648
+ if (error instanceof ProfileCaseCleanupError) {
1649
+ profiles.push(error.result);
1650
+ cleanupOwners.push(error.owner);
1651
+ } else {
1652
+ if (cleanupOwners.length === 0) throw error;
1653
+ profiles.push({
1654
+ profile: { id: profile.id, version: profile.version },
1655
+ status: "incomplete",
1656
+ ok: false,
1657
+ phase: "fixtures",
1658
+ portability: [],
1659
+ fixtures: [],
1660
+ error: "Profile execution failed"
1661
+ });
1662
+ }
1663
+ }
1664
+ }
1665
+ const report = {
1666
+ formatVersion: 1,
1667
+ kind: "mcpfn.client-profile-contract-report",
1668
+ status: profiles.some(({ status }) => status === "incomplete") ? "incomplete" : "complete",
1669
+ ok: profiles.every(({ ok }) => ok),
1670
+ profiles,
1671
+ droppedProfiles: 0
1672
+ };
1673
+ if (new TextEncoder().encode(JSON.stringify(report)).byteLength <= maxReportBytes)
1674
+ return finishReport(report, cleanupOwners);
1675
+ report.ok = false;
1676
+ report.status = "incomplete";
1677
+ report.incompleteReason = "Report content exceeded maxReportBytes and was truncated";
1678
+ while (new TextEncoder().encode(JSON.stringify(report)).byteLength > maxReportBytes && report.profiles.length > 0) {
1679
+ report.profiles.pop();
1680
+ report.droppedProfiles += 1;
1681
+ }
1682
+ if (new TextEncoder().encode(JSON.stringify(report)).byteLength > maxReportBytes) {
1683
+ throw new Error("The minimum client profile report exceeds maxReportBytes");
1684
+ }
1685
+ return finishReport(report, cleanupOwners);
1686
+ }
1687
+ function finishReport(report, cleanupOwners) {
1688
+ if (cleanupOwners.length) throw new McpFnClientProfileContractCleanupError(report, cleanupOwners);
1689
+ return report;
1690
+ }
1691
+
1692
+ // src/conformance-process.ts
995
1693
  var import_node_child_process = require("child_process");
1694
+ var import_node_path = __toESM(require("path"), 1);
1695
+ async function terminateConformanceRunner(child, platform = process.platform, systemRoot = process.env.SystemRoot) {
1696
+ const killWrapper = () => {
1697
+ try {
1698
+ child.kill("SIGKILL");
1699
+ } catch {
1700
+ }
1701
+ };
1702
+ try {
1703
+ if (platform === "win32" && child.pid) {
1704
+ if (!systemRoot || !import_node_path.default.win32.isAbsolute(systemRoot)) {
1705
+ killWrapper();
1706
+ return;
1707
+ }
1708
+ await new Promise((resolve) => {
1709
+ const timer = setTimeout(() => {
1710
+ killWrapper();
1711
+ resolve();
1712
+ }, 2500);
1713
+ const done = (error) => {
1714
+ clearTimeout(timer);
1715
+ if (error) killWrapper();
1716
+ resolve();
1717
+ };
1718
+ try {
1719
+ (0, import_node_child_process.execFile)(
1720
+ import_node_path.default.win32.join(systemRoot, "System32", "taskkill.exe"),
1721
+ ["/pid", String(child.pid), "/T", "/F"],
1722
+ { timeout: 2e3, windowsHide: true },
1723
+ done
1724
+ );
1725
+ } catch (error) {
1726
+ done(error);
1727
+ }
1728
+ });
1729
+ } else if (child.pid) {
1730
+ try {
1731
+ process.kill(-child.pid, "SIGKILL");
1732
+ } catch (error) {
1733
+ if (error.code !== "ESRCH") killWrapper();
1734
+ }
1735
+ } else killWrapper();
1736
+ } finally {
1737
+ child.stdout?.destroy();
1738
+ child.stderr?.destroy();
1739
+ }
1740
+ }
1741
+
1742
+ // src/conformance.ts
1743
+ var import_node_child_process2 = require("child_process");
1744
+ var import_node_crypto3 = require("crypto");
996
1745
  var import_node_fs = require("fs");
997
1746
  var import_node_http2 = require("http");
998
1747
  var import_node_https = require("https");
999
- var import_node_path = __toESM(require("path"), 1);
1748
+ var import_node_path2 = __toESM(require("path"), 1);
1749
+ var import_oauth_core5 = require("@superfunctions/oauth-core");
1750
+
1751
+ // src/remote-target.ts
1752
+ var import_client3 = require("@mcpfn/client");
1753
+ var import_oauth_core3 = require("@superfunctions/oauth-core");
1754
+ var targetSecrets = /* @__PURE__ */ new WeakMap();
1755
+ var utf8Encoder = new TextEncoder();
1756
+ function beginTargetCredentialRedaction(target) {
1757
+ const state = targetSecrets.get(target);
1758
+ if (!state) return () => void 0;
1759
+ const scope = new Set(state.active.keys());
1760
+ state.scopes.add(scope);
1761
+ return () => {
1762
+ state.scopes.delete(scope);
1763
+ scope.clear();
1764
+ };
1765
+ }
1766
+ function credentialValues(headers) {
1767
+ const values = boundedCredentialEntries(headers);
1768
+ const secrets = /* @__PURE__ */ new Set();
1769
+ for (const [header, raw] of values) {
1770
+ if (!raw.trim()) continue;
1771
+ secrets.add(raw);
1772
+ secrets.add(raw.trim());
1773
+ const normalizedHeader = header.toLowerCase();
1774
+ if (["authorization", "proxy-authorization"].includes(normalizedHeader)) {
1775
+ addAuthorizationSecrets(raw, secrets);
1776
+ }
1777
+ if (normalizedHeader === "cookie") addCookieSecrets(raw, secrets);
1778
+ }
1779
+ return secrets;
1780
+ }
1781
+ function addAuthorizationSecrets(raw, secrets) {
1782
+ const trimmed = raw.trim();
1783
+ const separator = trimmed.search(/\s/);
1784
+ if (separator <= 0) return;
1785
+ const scheme = trimmed.slice(0, separator).toLowerCase();
1786
+ const token = trimmed.slice(separator).trim();
1787
+ if (token) secrets.add(token);
1788
+ if (scheme !== "basic") {
1789
+ if (scheme !== "bearer" && !authorizationToken68.test(token)) {
1790
+ addAuthorizationParameterSecrets(token, secrets);
1791
+ }
1792
+ return;
1793
+ }
1794
+ addBasicAuthorizationSecrets(token, secrets);
1795
+ }
1796
+ function addBasicAuthorizationSecrets(token, secrets) {
1797
+ if (!/^[\dA-Za-z+/]+={0,2}$/.test(token)) return;
1798
+ const decoded = Buffer.from(token, "base64");
1799
+ if (stripBase64Padding(decoded.toString("base64")) !== stripBase64Padding(token)) return;
1800
+ for (const encoding of ["utf8", "latin1"]) {
1801
+ const pair = decoded.toString(encoding);
1802
+ const colon = pair.indexOf(":");
1803
+ if (colon < 0) continue;
1804
+ for (const value of [pair, pair.slice(0, colon), pair.slice(colon + 1)]) {
1805
+ if (value) secrets.add(value);
1806
+ }
1807
+ }
1808
+ }
1809
+ var authorizationToken68 = /^[\dA-Za-z._~+\x2F-]+=*$/;
1810
+ var authorizationParameterName = /^[!#$%&'*+.^_`|~\dA-Za-z-]+$/;
1811
+ function skipAuthorizationWhitespace(token, cursor) {
1812
+ while (token[cursor] === " " || token[cursor] === " ") cursor += 1;
1813
+ return cursor;
1814
+ }
1815
+ function parseQuotedAuthorizationValue(token, start) {
1816
+ let cursor = start + 1;
1817
+ let decoded = "";
1818
+ while (cursor < token.length && token[cursor] !== '"') {
1819
+ if (token[cursor] === "\\") {
1820
+ cursor += 1;
1821
+ if (cursor >= token.length) {
1822
+ throw new TypeError("Authorization credential parameters must use balanced quotes");
1823
+ }
1824
+ }
1825
+ decoded += token[cursor];
1826
+ cursor += 1;
1827
+ }
1828
+ if (token[cursor] !== '"') {
1829
+ throw new TypeError("Authorization credential parameters must use balanced quotes");
1830
+ }
1831
+ cursor += 1;
1832
+ const rawValue = token.slice(start, cursor);
1833
+ return {
1834
+ cursor,
1835
+ components: [rawValue, rawValue.slice(1, -1), decoded].filter(Boolean)
1836
+ };
1837
+ }
1838
+ function parseUnquotedAuthorizationValue(token, start) {
1839
+ let cursor = start;
1840
+ while (cursor < token.length && token[cursor] !== ",") cursor += 1;
1841
+ const value = token.slice(start, cursor).trim();
1842
+ if (!value || /\s/.test(value) || value.includes('"')) {
1843
+ throw new TypeError("Authorization credential parameters must contain valid values");
1844
+ }
1845
+ return { cursor, components: [value] };
1846
+ }
1847
+ function parseAuthorizationParameter(token, start) {
1848
+ let cursor = skipAuthorizationWhitespace(token, start);
1849
+ const nameStart = cursor;
1850
+ while (cursor < token.length && token[cursor] !== "=" && token[cursor] !== " " && token[cursor] !== " " && token[cursor] !== ",") cursor += 1;
1851
+ const name = token.slice(nameStart, cursor);
1852
+ cursor = skipAuthorizationWhitespace(token, cursor);
1853
+ if (!authorizationParameterName.test(name) || token[cursor] !== "=") {
1854
+ throw new TypeError("Authorization credential parameters must use name=value syntax");
1855
+ }
1856
+ cursor = skipAuthorizationWhitespace(token, cursor + 1);
1857
+ if (cursor >= token.length) {
1858
+ throw new TypeError("Authorization credential parameters must contain values");
1859
+ }
1860
+ const parsed = token[cursor] === '"' ? parseQuotedAuthorizationValue(token, cursor) : parseUnquotedAuthorizationValue(token, cursor);
1861
+ return { ...parsed, cursor: skipAuthorizationWhitespace(token, parsed.cursor) };
1862
+ }
1863
+ function addAuthorizationParameterSecrets(token, secrets) {
1864
+ const firstEquals = token.indexOf("=");
1865
+ if (firstEquals < 1 || !authorizationParameterName.test(token.slice(0, firstEquals).trim())) return;
1866
+ let cursor = 0;
1867
+ while (cursor < token.length) {
1868
+ const parsed = parseAuthorizationParameter(token, cursor);
1869
+ for (const component of parsed.components) secrets.add(component);
1870
+ cursor = parsed.cursor;
1871
+ if (cursor === token.length) return;
1872
+ if (token[cursor] !== ",") {
1873
+ throw new TypeError("Authorization credential parameters must be comma separated");
1874
+ }
1875
+ cursor = skipAuthorizationWhitespace(token, cursor + 1);
1876
+ if (cursor === token.length) {
1877
+ throw new TypeError("Authorization credential parameters must not end with a comma");
1878
+ }
1879
+ }
1880
+ }
1881
+ function stripBase64Padding(value) {
1882
+ let end = value.length;
1883
+ while (end > 0 && value[end - 1] === "=") end -= 1;
1884
+ return value.slice(0, end);
1885
+ }
1886
+ function addCookieSecrets(raw, secrets) {
1887
+ for (const part of raw.split(/[;,]/)) {
1888
+ const pair = part.trim();
1889
+ if (!pair) continue;
1890
+ const equals = pair.indexOf("=");
1891
+ if (equals < 1) {
1892
+ throw new TypeError("Cookie credential headers must contain name=value pairs");
1893
+ }
1894
+ const value = pair.slice(equals + 1).trim();
1895
+ if (!value) continue;
1896
+ secrets.add(value);
1897
+ addUnquotedCookieSecret(value, secrets);
1898
+ }
1899
+ }
1900
+ function addUnquotedCookieSecret(value, secrets) {
1901
+ if (!value.startsWith('"') && !value.endsWith('"')) return;
1902
+ if (value.length < 2 || !value.startsWith('"') || !value.endsWith('"')) {
1903
+ throw new TypeError("Cookie credential values must use balanced quotes");
1904
+ }
1905
+ const unquoted = value.slice(1, -1);
1906
+ if (unquoted.includes('"') || unquoted.includes("\\")) {
1907
+ throw new TypeError("Cookie credential values must use RFC 6265 syntax");
1908
+ }
1909
+ if (unquoted) secrets.add(unquoted);
1910
+ }
1911
+ var McpFnRedactionLimitError = class extends Error {
1912
+ constructor(message = "Credential redaction exceeded its traversal budget") {
1913
+ super(message);
1914
+ }
1915
+ };
1916
+ var McpFnStructuralCredentialCollisionError = class extends McpFnRedactionLimitError {
1917
+ constructor() {
1918
+ super("Credential collides with required artifact structure");
1919
+ }
1920
+ };
1921
+ var envelopeKeys = Object.fromEntries(Object.entries({
1922
+ root: "formatVersion kind artifactValidation status runtime ok target server capabilities manifestChecked manifestHash total passed failed incomplete droppedResults droppedObservedEvents redactionOmittedObservedEvents incompleteReason failure timeline droppedTimelineEvents results count clientState tools resources resourceTemplates prompts droppedEvents timelineComplete droppedInventoryEntries inventoryComplete suiteVersion exitCode stdout stderr phase outcome code requestId at details payload",
1923
+ result: "formatVersion name operation tool status sideEffect durationMs error droppedObservedEvents redactionOmittedObservedEvents",
1924
+ diagnostic: "phase outcome code requestId at target details",
1925
+ inspectorEvent: "formatVersion source kind at event",
1926
+ failure: "name message layer code phase details",
1927
+ runtime: "node scenarioFormatVersion reportSchemaVersion packages",
1928
+ packages: "testing",
1929
+ droppedInventoryEntries: "tools resources resourceTemplates prompts",
1930
+ target: "kind"
1931
+ }).map(([role, keys]) => [role, new Set(keys.split(" "))]));
1932
+ var structuralArtifactValues = new Map([
1933
+ ["root:kind", /* @__PURE__ */ new Set(["logging.message", "progress", "tasks.status", "resources.updated", "tools.list_changed", "resources.list_changed", "prompts.list_changed", "resources.subscribed", "resources.unsubscribed", "client.roots", "client.sampling", "client.elicitation", "mcpfn.target-suite-report", "mcpfn.inspector-snapshot", "mcpfn.official-conformance-report"])],
1934
+ ["inspectorEvent:kind", /* @__PURE__ */ new Set(["logging.message", "progress", "tasks.status", "resources.updated", "tools.list_changed", "resources.list_changed", "prompts.list_changed", "resources.subscribed", "resources.unsubscribed", "client.roots", "client.sampling", "client.elicitation"])],
1935
+ ["root:clientState", /* @__PURE__ */ new Set(["idle", "connecting", "authorization-required", "connected", "closing", "closed"])],
1936
+ ["target:kind", /* @__PURE__ */ new Set(["authenticated-streamable-http", "streamable-http", "stdio", "in-memory", "custom"])],
1937
+ ["root:status", /* @__PURE__ */ new Set(["passed", "failed", "incomplete", "complete"])],
1938
+ ["result:status", /* @__PURE__ */ new Set(["passed", "failed", "incomplete", "complete"])],
1939
+ ["root:outcome", /* @__PURE__ */ new Set(["started", "succeeded", "failed"])],
1940
+ ["diagnostic:outcome", /* @__PURE__ */ new Set(["started", "succeeded", "failed"])],
1941
+ ...["root", "diagnostic", "failure"].map((role) => [
1942
+ `${role}:phase`,
1943
+ /* @__PURE__ */ new Set(["resource-discovery", "authorization-server-discovery", "client-registration", "authorization-request", "authorization-callback", "token-exchange", "token-refresh", "token-revocation", "transport-connect", "mcp-initialize", "capability-operation", "transport-close"])
1944
+ ]),
1945
+ ["failure:layer", /* @__PURE__ */ new Set(["mcpfn-preflight", "authorization-server", "resource-server", "mcp-initialization", "scenario", "upstream-conformance"])],
1946
+ ["inspectorEvent:source", /* @__PURE__ */ new Set(["diagnostic", "client"])],
1947
+ ["result:sideEffect", /* @__PURE__ */ new Set(["none", "idempotent", "non-idempotent"])]
1948
+ ]);
1949
+ var unrestrictedStructuralValueFields = /* @__PURE__ */ new Set([
1950
+ "packages:testing",
1951
+ "runtime:node",
1952
+ "runtime:reportSchemaVersion",
1953
+ "root:suiteVersion"
1954
+ ]);
1955
+ function childEnvelopeRole(fixed, key, role, input, rootKind) {
1956
+ if (!fixed) return "payload";
1957
+ if (key === "results") return "result";
1958
+ if (key === "timeline") {
1959
+ return rootKind === "mcpfn.inspector-snapshot" ? "inspectorEvent" : "diagnostic";
1960
+ }
1961
+ if (["failure", "runtime", "packages", "droppedInventoryEntries", "target"].includes(key)) {
1962
+ return key;
1963
+ }
1964
+ if (role === "inspectorEvent" && key === "event") {
1965
+ return input.source === "client" ? "root" : "diagnostic";
1966
+ }
1967
+ return "payload";
1968
+ }
1969
+ function specialValue(input) {
1970
+ if (input instanceof Error) return normalizeErrorValue(input);
1971
+ if (input instanceof Date) return Number.isNaN(input.getTime()) ? "Invalid Date" : input.toISOString();
1972
+ if (input instanceof URL) return input.href;
1973
+ if (input instanceof Map) {
1974
+ if (Object.getOwnPropertyDescriptor(Map.prototype, "size").get.call(input) > 1e5) throw new McpFnRedactionLimitError();
1975
+ return { type: "Map", entries: [...Map.prototype.entries.call(input)] };
1976
+ }
1977
+ if (input instanceof Set) {
1978
+ if (Object.getOwnPropertyDescriptor(Set.prototype, "size").get.call(input) > 1e5) throw new McpFnRedactionLimitError();
1979
+ return { type: "Set", values: [...Set.prototype.values.call(input)] };
1980
+ }
1981
+ return input;
1982
+ }
1983
+ function normalizeErrorValue(input) {
1984
+ const normalized = /* @__PURE__ */ Object.create(null);
1985
+ let enumerableFields = 0;
1986
+ for (const key in input) {
1987
+ if (!Object.hasOwn(input, key)) continue;
1988
+ if (++enumerableFields > 1e5) throw new McpFnRedactionLimitError();
1989
+ normalized[key] = input[key];
1990
+ }
1991
+ normalized.name = input.name;
1992
+ normalized.message = input.message;
1993
+ normalized.stack = input.stack;
1994
+ if (input.cause !== void 0) normalized.cause = input.cause;
1995
+ return normalized;
1996
+ }
1997
+ function credentialPatterns(values) {
1998
+ const variants = [...values].filter(Boolean).flatMap((secret) => [
1999
+ { value: secret, encoded: false },
2000
+ { value: JSON.stringify(secret).slice(1, -1), encoded: false },
2001
+ { value: encodeURIComponent(secret), encoded: true },
2002
+ { value: new URLSearchParams({ value: secret }).toString().slice("value=".length), encoded: true }
2003
+ ]).sort((a, b) => b.value.length - a.value.length);
2004
+ const patterns = variants.map(({ value, encoded }) => {
2005
+ const literal = value.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
2006
+ return encoded ? literal.replace(/%[\dA-Fa-f]{2}/g, (escape) => escape.replace(/[A-Fa-f]/g, (hex) => `[${hex.toLowerCase()}${hex.toUpperCase()}]`)) : literal;
2007
+ });
2008
+ return {
2009
+ secretPattern: patterns.length ? new RegExp(patterns.join("|"), "g") : void 0,
2010
+ replacementPattern: patterns.length ? new RegExp(patterns.join("|")) : void 0
2011
+ };
2012
+ }
2013
+ function targetCredentialValues(target) {
2014
+ const state = targetSecrets.get(target);
2015
+ return /* @__PURE__ */ new Set([
2016
+ ...state?.active.keys() ?? [],
2017
+ ...[...state?.scopes ?? []].flatMap((scope) => [...scope])
2018
+ ]);
2019
+ }
2020
+ function scrubCredentials(value, values, preserveKeys = false, redactionMarker) {
2021
+ let entries = 0, stringBytes = 0;
2022
+ const budget = (input, depth = 0) => {
2023
+ input = specialValue(input);
2024
+ if (Array.isArray(input) && input.length > 1e5 || ++entries > 1e5 || depth > 32) throw new McpFnRedactionLimitError();
2025
+ if (typeof input === "string") {
2026
+ stringBytes += Buffer.byteLength(input);
2027
+ if (input.length > 262144 || stringBytes > 2097152) throw new McpFnRedactionLimitError();
2028
+ } else if (input && typeof input === "object") {
2029
+ for (const key in input) if (Object.hasOwn(input, key)) {
2030
+ budget(key, depth + 1);
2031
+ budget(input[key], depth + 1);
2032
+ }
2033
+ }
2034
+ };
2035
+ budget(value);
2036
+ const { secretPattern, replacementPattern } = credentialPatterns(values);
2037
+ const preserveStructural = (input) => {
2038
+ if (replacementPattern?.test(input)) {
2039
+ throw new McpFnRedactionLimitError(
2040
+ "Credential collides with a required structural artifact field"
2041
+ );
2042
+ }
2043
+ return input;
2044
+ };
2045
+ const preserveStructuralKey = (input) => {
2046
+ if (replacementPattern?.test(input)) {
2047
+ throw new McpFnStructuralCredentialCollisionError();
2048
+ }
2049
+ return input;
2050
+ };
2051
+ if (secretPattern) {
2052
+ const markerContainsSecret = secretPattern.test(redactionMarker ?? "[REDACTED]");
2053
+ secretPattern.lastIndex = 0;
2054
+ if (markerContainsSecret) redactionMarker = "";
2055
+ }
2056
+ const scrubString = (input, role2, field, finalPass) => {
2057
+ const structuralField = `${role2}:${field}`;
2058
+ if (unrestrictedStructuralValueFields.has(structuralField) || structuralArtifactValues.get(structuralField)?.has(input)) {
2059
+ return preserveStructural(input);
2060
+ }
2061
+ if (finalPass) return replacementPattern?.test(input) ? "" : input;
2062
+ if (!secretPattern) return input;
2063
+ return input.replace(secretPattern, (secret) => {
2064
+ const maskCharacter = secret.includes("*") ? "#" : "*";
2065
+ const defaultReplacement = secret.length < 10 ? maskCharacter.repeat(secret.length) : "[REDACTED]";
2066
+ const replacement = redactionMarker ?? defaultReplacement;
2067
+ return replacementPattern.test(replacement) ? "" : replacement;
2068
+ });
2069
+ };
2070
+ function scrubArray(input, role2, finalPass) {
2071
+ const result2 = input.map((entry) => scrub(entry, role2, "", finalPass));
2072
+ assertPayloadSerialization(result2, role2, finalPass, replacementPattern);
2073
+ return result2;
2074
+ }
2075
+ function scrubObject(input, role2, finalPass) {
2076
+ const entries2 = [];
2077
+ const keys = /* @__PURE__ */ new Set();
2078
+ for (const [key, entry] of Object.entries(input)) {
2079
+ const fixed = envelopeKeys[role2]?.has(key) ?? false;
2080
+ if (fixed) preserveStructuralKey(key);
2081
+ const childRole = childEnvelopeRole(
2082
+ fixed,
2083
+ key,
2084
+ role2,
2085
+ input,
2086
+ value?.kind
2087
+ );
2088
+ const scrubbedKey = fixed ? key : scrub(key, "payload", "", finalPass);
2089
+ if (keys.has(scrubbedKey)) {
2090
+ throw new McpFnRedactionLimitError(
2091
+ "Credential redaction created duplicate object keys"
2092
+ );
2093
+ }
2094
+ keys.add(scrubbedKey);
2095
+ const entryRole = fixed ? role2 : "payload";
2096
+ const scrubbedEntry = typeof entry === "string" ? scrub(entry, entryRole, key, finalPass) : scrub(entry, childRole, "", finalPass);
2097
+ entries2.push([scrubbedKey, scrubbedEntry]);
2098
+ }
2099
+ const result2 = Object.fromEntries(entries2);
2100
+ assertPayloadSerialization(result2, role2, finalPass, replacementPattern);
2101
+ return result2;
2102
+ }
2103
+ function scrub(input, role2 = "payload", field = "", finalPass = false) {
2104
+ input = specialValue(input);
2105
+ if (typeof input === "string") return scrubString(input, role2, field, finalPass);
2106
+ if (Array.isArray(input)) return scrubArray(input, role2, finalPass);
2107
+ if (input && typeof input === "object") {
2108
+ return scrubObject(input, role2, finalPass);
2109
+ }
2110
+ return input;
2111
+ }
2112
+ const timelineEvent = value && typeof value === "object" && ["client", "diagnostic"].includes(value.source ?? "");
2113
+ let role = "payload";
2114
+ if (preserveKeys) role = timelineEvent ? "inspectorEvent" : "root";
2115
+ const scrubbed = scrub(value, role);
2116
+ const generic = (0, import_oauth_core3.redactOAuthValue)(scrubbed, { maxStringLength: 262144, maxDepth: 64, maxArrayEntries: 1e5, maxObjectEntries: 1e5, ...redactionMarker !== void 0 ? { redactionMarker } : {} });
2117
+ const result = scrub(generic, role, "", true);
2118
+ assertPayloadSerialization(
2119
+ result,
2120
+ "payload",
2121
+ true,
2122
+ replacementPattern
2123
+ );
2124
+ return result;
2125
+ }
2126
+ function assertPayloadSerialization(value, role, finalPass, credentialPattern) {
2127
+ if (!finalPass || role !== "payload" || !credentialPattern) return;
2128
+ const serialized = JSON.stringify(value);
2129
+ if (serialized !== void 0 && credentialPattern.test(serialized)) {
2130
+ throw new McpFnRedactionLimitError(
2131
+ "Credential redaction could not guarantee safe serialized output"
2132
+ );
2133
+ }
2134
+ }
2135
+ function redactTargetCredentials(target, value, options = {}) {
2136
+ return scrubCredentials(
2137
+ value,
2138
+ targetCredentialValues(target),
2139
+ options.preserveKeys,
2140
+ options.redactionMarker
2141
+ );
2142
+ }
2143
+ function assertTargetArtifactSafe(target, value) {
2144
+ const { replacementPattern } = credentialPatterns(targetCredentialValues(target));
2145
+ if (replacementPattern?.test(value)) {
2146
+ throw new McpFnRedactionLimitError(
2147
+ "Credential redaction could not guarantee safe serialized output"
2148
+ );
2149
+ }
2150
+ }
2151
+ function redactRemoteCredential(credential, value, options = {}) {
2152
+ return scrubCredentials(value, credentialValues(credential.headers), options.preserveKeys);
2153
+ }
2154
+ var MAX_CREDENTIAL_HEADERS = 32;
2155
+ var MAX_CREDENTIAL_HEADER_BYTES = 16384;
2156
+ var MAX_CREDENTIAL_HEADER_VALUE_BYTES = 8192;
2157
+ var FORBIDDEN_CREDENTIAL_HEADERS = /* @__PURE__ */ new Set([
2158
+ "connection",
2159
+ "content-length",
2160
+ "host",
2161
+ "proxy-connection",
2162
+ "te",
2163
+ "trailer",
2164
+ "transfer-encoding",
2165
+ "upgrade"
2166
+ ]);
2167
+ function staticRemoteCredentialProvider(credential) {
2168
+ return { acquire: () => credential };
2169
+ }
2170
+ async function acquireRemoteCredential(source, context) {
2171
+ let provider;
2172
+ let acquired;
2173
+ try {
2174
+ provider = isCredentialProvider(source) ? source : staticRemoteCredentialProvider(source);
2175
+ acquired = await provider.acquire(context);
2176
+ } catch {
2177
+ throw new Error("Target credential acquisition failed");
2178
+ }
2179
+ let credential = acquired;
2180
+ try {
2181
+ credential = { ...acquired, headers: boundedCredentialEntries(acquired.headers) };
2182
+ } catch {
2183
+ }
2184
+ let releasePromise;
2185
+ let revoked = false;
2186
+ let disposed = false;
2187
+ let cleanupPhase = credential.kind === "oauth" ? "token-revocation" : "transport-close";
2188
+ return {
2189
+ credential,
2190
+ cleanupPhase: () => cleanupPhase,
2191
+ release() {
2192
+ releasePromise ??= (async () => {
2193
+ const cleanupContext = { ...context, signal: new AbortController().signal };
2194
+ try {
2195
+ if (!revoked) {
2196
+ cleanupPhase = credential.kind === "oauth" ? "token-revocation" : "transport-close";
2197
+ await provider.revoke?.(acquired, cleanupContext);
2198
+ revoked = true;
2199
+ }
2200
+ if (!disposed) {
2201
+ cleanupPhase = "transport-close";
2202
+ await provider.dispose?.(acquired, cleanupContext);
2203
+ disposed = true;
2204
+ }
2205
+ } catch {
2206
+ throw new import_client3.McpFnClientError(
2207
+ "MCPFN_OPERATION_FAILED",
2208
+ "Target credential cleanup failed",
2209
+ { phase: cleanupPhase, retryable: true }
2210
+ );
2211
+ }
2212
+ })().catch((error) => {
2213
+ releasePromise = void 0;
2214
+ throw error;
2215
+ });
2216
+ return releasePromise;
2217
+ }
2218
+ };
2219
+ }
2220
+ function authenticatedHttpTarget(url, options) {
2221
+ const targetUrl = normalizeRemoteTargetUrl(url);
2222
+ const descriptorUrl = new URL(targetUrl);
2223
+ descriptorUrl.search = "";
2224
+ const { credential: _credential, requestInit, ...transportOptions } = options;
2225
+ const state = { active: /* @__PURE__ */ new Map(), scopes: /* @__PURE__ */ new Set() };
2226
+ const pendingReleases = /* @__PURE__ */ new Set();
2227
+ const authenticated = (0, import_client3.customTarget)({
2228
+ kind: "authenticated-streamable-http",
2229
+ descriptor: {
2230
+ url: descriptorUrl.toString(),
2231
+ authenticated: true
2232
+ },
2233
+ redact: (value, redaction) => redactTargetCredentials(authenticated, value, { preserveKeys: true, ...redaction }),
2234
+ assertArtifactSafe: (value) => assertTargetArtifactSafe(authenticated, value),
2235
+ beginRedactionScope: () => beginTargetCredentialRedaction(authenticated),
2236
+ async cleanup() {
2237
+ const results = await Promise.allSettled([...pendingReleases].map((release) => release()));
2238
+ const failures = results.filter((result) => result.status === "rejected");
2239
+ const failure = failures.find(({ reason }) => reason && typeof reason === "object" && reason.phase === "token-revocation") ?? failures[0];
2240
+ if (failure) throw failure.reason;
2241
+ },
2242
+ async open(targetContext) {
2243
+ const context = {
2244
+ url: targetUrl.toString(),
2245
+ requestId: targetContext.requestId,
2246
+ signal: targetContext.signal
2247
+ };
2248
+ const lease = await acquireRemoteCredential(options.credential, context);
2249
+ const secrets = /* @__PURE__ */ new Set();
2250
+ const release = async () => {
2251
+ try {
2252
+ await lease.release();
2253
+ } catch (error) {
2254
+ pendingReleases.add(release);
2255
+ throw error;
2256
+ }
2257
+ pendingReleases.delete(release);
2258
+ for (const secret of secrets) {
2259
+ const count = (state.active.get(secret) ?? 1) - 1;
2260
+ if (count) state.active.set(secret, count);
2261
+ else state.active.delete(secret);
2262
+ }
2263
+ secrets.clear();
2264
+ };
2265
+ let handle;
2266
+ try {
2267
+ for (const secret of credentialValues(lease.credential.headers)) {
2268
+ secrets.add(secret);
2269
+ state.active.set(secret, (state.active.get(secret) ?? 0) + 1);
2270
+ for (const scope of state.scopes) scope.add(secret);
2271
+ }
2272
+ const credentialHeaders = validateRemoteCredentialHeaders(lease.credential.headers);
2273
+ const headers = new Headers(requestInit?.headers);
2274
+ credentialHeaders.forEach((value, name) => headers.set(name, value));
2275
+ const target = (0, import_client3.streamableHttpTarget)(targetUrl, {
2276
+ ...transportOptions,
2277
+ requestInit: {
2278
+ ...requestInit,
2279
+ headers,
2280
+ // Fetch must not replay credentials to a redirect destination.
2281
+ redirect: "error"
2282
+ }
2283
+ });
2284
+ handle = await target.open(targetContext);
2285
+ } catch (error) {
2286
+ let message = "Authenticated target could not be opened";
2287
+ try {
2288
+ message = redactRemoteCredential(lease.credential, error instanceof Error ? error.message : String(error));
2289
+ } catch {
2290
+ }
2291
+ pendingReleases.add(release);
2292
+ await release();
2293
+ throw new Error(message);
2294
+ }
2295
+ let closePromise;
2296
+ let transportClosed = false;
2297
+ return {
2298
+ transport: handle.transport,
2299
+ finishAuthorization: handle.finishAuthorization,
2300
+ terminateSession: handle.terminateSession,
2301
+ close() {
2302
+ closePromise ??= Promise.resolve().then(async () => {
2303
+ if (!transportClosed) {
2304
+ if (handle.close) await handle.close();
2305
+ else await handle.transport.close();
2306
+ transportClosed = true;
2307
+ }
2308
+ await release();
2309
+ }).catch(async (error) => {
2310
+ closePromise = void 0;
2311
+ const phase = transportClosed ? lease.cleanupPhase() : "transport-close";
2312
+ await targetContext.diagnostic({
2313
+ phase,
2314
+ outcome: "failed",
2315
+ code: "MCPFN_CREDENTIAL_CLEANUP_FAILED",
2316
+ requestId: targetContext.requestId,
2317
+ at: (/* @__PURE__ */ new Date()).toISOString(),
2318
+ target: { kind: "authenticated-streamable-http", url: descriptorUrl.toString() },
2319
+ details: { message: "Target credential cleanup failed" }
2320
+ });
2321
+ throw error;
2322
+ });
2323
+ return closePromise;
2324
+ }
2325
+ };
2326
+ }
2327
+ });
2328
+ targetSecrets.set(authenticated, state);
2329
+ return authenticated;
2330
+ }
2331
+ function normalizeRemoteTargetUrl(value) {
2332
+ const url = new URL(value.toString());
2333
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
2334
+ throw new TypeError("Authenticated MCP targets must use HTTP or HTTPS");
2335
+ }
2336
+ if (url.protocol === "http:" && url.hostname !== "[::1]" && !/^127(?:\.\d{1,3}){3}$/.test(url.hostname)) {
2337
+ throw new TypeError("Credential-bearing HTTP targets require a literal loopback address; use HTTPS remotely");
2338
+ }
2339
+ if (url.username || url.password || url.hash) {
2340
+ throw new TypeError(
2341
+ "Authenticated MCP target URLs must not contain userinfo or a fragment"
2342
+ );
2343
+ }
2344
+ return url;
2345
+ }
2346
+ function isCredentialProvider(value) {
2347
+ return "acquire" in value && typeof value.acquire === "function";
2348
+ }
2349
+ function boundedCredentialEntries(value) {
2350
+ const entries = [];
2351
+ let bytes2 = 0;
2352
+ const append = (name, headerValue) => {
2353
+ if (entries.length >= MAX_CREDENTIAL_HEADERS) throw new TypeError("Credential headers exceed the header limit");
2354
+ if (typeof name !== "string" || typeof headerValue !== "string") throw new TypeError("Credential header values must be strings");
2355
+ if (name.length > MAX_CREDENTIAL_HEADER_BYTES || headerValue.length > MAX_CREDENTIAL_HEADER_VALUE_BYTES) throw new TypeError("Credential header exceeds the value-size limit");
2356
+ if (utf8Encoder.encode(headerValue).byteLength > MAX_CREDENTIAL_HEADER_VALUE_BYTES) throw new TypeError("Credential header exceeds the value-size limit");
2357
+ bytes2 += utf8Encoder.encode(`${name}: ${headerValue}\r
2358
+ `).byteLength;
2359
+ if (bytes2 > MAX_CREDENTIAL_HEADER_BYTES) throw new TypeError("Credential headers exceed the aggregate size limit");
2360
+ entries.push([name, headerValue]);
2361
+ };
2362
+ if (value instanceof Headers) value.forEach((v, k) => append(k, v));
2363
+ else if (Array.isArray(value)) {
2364
+ if (value.length > MAX_CREDENTIAL_HEADERS) throw new TypeError("Credential headers exceed the header limit");
2365
+ for (const entry of value) append(entry[0], entry[1]);
2366
+ } else {
2367
+ for (const name in value) if (Object.hasOwn(value, name)) append(name, value[name]);
2368
+ }
2369
+ return entries;
2370
+ }
2371
+ function validateRemoteCredentialHeaders(value) {
2372
+ const headers = new Headers(boundedCredentialEntries(value));
2373
+ const entries = [];
2374
+ headers.forEach((headerValue, name) => entries.push([name, headerValue]));
2375
+ if (entries.length === 0) {
2376
+ throw new TypeError("Authenticated MCP targets require at least one credential header");
2377
+ }
2378
+ if (entries.length > MAX_CREDENTIAL_HEADERS) {
2379
+ throw new TypeError(`Credential headers exceed the ${MAX_CREDENTIAL_HEADERS}-header limit`);
2380
+ }
2381
+ let bytes2 = 0;
2382
+ for (const [name, headerValue] of entries) {
2383
+ if (!headerValue.trim()) throw new TypeError("Credential header values must not be blank");
2384
+ if (FORBIDDEN_CREDENTIAL_HEADERS.has(name)) {
2385
+ throw new TypeError(`Credential header ${name} is not allowed`);
2386
+ }
2387
+ const valueBytes = utf8Encoder.encode(headerValue).byteLength;
2388
+ if (valueBytes > MAX_CREDENTIAL_HEADER_VALUE_BYTES) {
2389
+ throw new TypeError(`Credential header ${name} exceeds the value-size limit`);
2390
+ }
2391
+ bytes2 += utf8Encoder.encode(`${name}: ${headerValue}\r
2392
+ `).byteLength;
2393
+ }
2394
+ if (bytes2 > MAX_CREDENTIAL_HEADER_BYTES) {
2395
+ throw new TypeError("Credential headers exceed the aggregate size limit");
2396
+ }
2397
+ return headers;
2398
+ }
2399
+
2400
+ // src/reports.ts
2401
+ var import_oauth_core4 = require("@superfunctions/oauth-core");
2402
+
2403
+ // src/artifact-guards.ts
2404
+ var import_client4 = require("@mcpfn/client");
2405
+ var targetSuiteArtifactGuards = /* @__PURE__ */ new WeakMap();
2406
+ var targetSuiteArtifactFinalizer = new FinalizationRegistry((release) => {
2407
+ try {
2408
+ release();
2409
+ } catch {
2410
+ }
2411
+ });
2412
+ function registerMcpFnTargetSuiteArtifactGuard(report, validate, release, maxBytes) {
2413
+ const previous = targetSuiteArtifactGuards.get(report);
2414
+ if (previous) {
2415
+ targetSuiteArtifactFinalizer.unregister(report);
2416
+ targetSuiteArtifactGuards.delete(report);
2417
+ previous.release();
2418
+ }
2419
+ let active = true;
2420
+ const releaseOnce = () => {
2421
+ if (!active) return;
2422
+ active = false;
2423
+ release();
2424
+ };
2425
+ targetSuiteArtifactGuards.set(report, { maxBytes, validate, release: releaseOnce });
2426
+ targetSuiteArtifactFinalizer.register(report, releaseOnce, report);
2427
+ }
2428
+ function disposeMcpFnTargetSuiteArtifactGuard(report) {
2429
+ const guard = targetSuiteArtifactGuards.get(report);
2430
+ if (!guard) return;
2431
+ targetSuiteArtifactGuards.delete(report);
2432
+ targetSuiteArtifactFinalizer.unregister(report);
2433
+ guard.release();
2434
+ }
2435
+ function preserveMcpFnTargetSuiteArtifact(report, artifact) {
2436
+ const guard = targetSuiteArtifactGuards.get(report);
2437
+ if (!guard) {
2438
+ if (report.artifactValidation === "target-aware") {
2439
+ throw new import_client4.McpFnClientError(
2440
+ "MCPFN_OPERATION_FAILED",
2441
+ "MCP target artifact proof is unavailable",
2442
+ { phase: "capability-operation" }
2443
+ );
2444
+ }
2445
+ return artifact;
2446
+ }
2447
+ if (new TextEncoder().encode(artifact).byteLength > guard.maxBytes) {
2448
+ throw new Error("Serialized target suite artifact exceeds maxReportBytes");
2449
+ }
2450
+ try {
2451
+ if (!guard.validate(artifact)) throw new Error("unsafe serialized artifact");
2452
+ return artifact;
2453
+ } catch {
2454
+ throw new import_client4.McpFnClientError(
2455
+ "MCPFN_OPERATION_FAILED",
2456
+ "MCP artifact structure conflicts with credential redaction",
2457
+ { phase: "capability-operation" }
2458
+ );
2459
+ }
2460
+ }
2461
+
2462
+ // src/reports.ts
2463
+ var MCPFN_TESTING_VERSION = "0.0.6";
2464
+ var MCPFN_REPORT_SCHEMA_VERSION = "1.0.0";
2465
+ function serializeMcpFnTargetSuiteReport(report, options = {}) {
2466
+ const space = options.space ?? 0;
2467
+ if (!Number.isInteger(space) || space < 0 || space > 10) {
2468
+ throw new TypeError("space must be an integer from 0 through 10");
2469
+ }
2470
+ const serialized = JSON.stringify(report, null, space) + (options.trailingNewline ? "\n" : "");
2471
+ return preserveMcpFnTargetSuiteArtifact(report, serialized);
2472
+ }
2473
+ function disposeMcpFnTargetSuiteReport(report) {
2474
+ disposeMcpFnTargetSuiteArtifactGuard(report);
2475
+ }
2476
+ function objectRecord(value) {
2477
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2478
+ }
2479
+ function redactedFailureCause(error) {
2480
+ if (!error || typeof error !== "object") return void 0;
2481
+ const cause = error.cause;
2482
+ if (cause === void 0) return void 0;
2483
+ return objectRecord((0, import_oauth_core4.redactOAuthValue)(cause, {
2484
+ maxDepth: 4,
2485
+ maxArrayEntries: 20,
2486
+ maxObjectEntries: 20,
2487
+ maxStringLength: 1024
2488
+ }));
2489
+ }
2490
+ function resourceDenied(layer, record, cause) {
2491
+ if (layer === "authorization-server") return false;
2492
+ return [cause?.code, cause?.status, record.code, record.status].some((value) => [401, 403].includes(Number(value)));
2493
+ }
2494
+ function normalizeMcpFnReportFailure(error, fallbackPhase) {
2495
+ const redacted = (0, import_oauth_core4.redactOAuthValue)(error, {
2496
+ maxDepth: 6,
2497
+ maxArrayEntries: 50,
2498
+ maxObjectEntries: 50,
2499
+ maxStringLength: 2048
2500
+ });
2501
+ const record = objectRecord(redacted) ?? {};
2502
+ const phase = stringField(record.phase) ?? fallbackPhase;
2503
+ const details = objectRecord(record.details);
2504
+ let message = stringField(record.message) ?? String(
2505
+ (0, import_oauth_core4.redactOAuthValue)(error instanceof Error ? error.message : String(error))
2506
+ );
2507
+ const causeRecord = redactedFailureCause(error);
2508
+ const causeMessage = stringField(causeRecord?.message);
2509
+ if (causeMessage && !message.includes(causeMessage)) message = `${message}: ${causeMessage}`;
2510
+ const combinedDetails = causeRecord ? { ...details, cause: causeRecord } : details;
2511
+ const layer = failureLayer(phase);
2512
+ const deniedByResource = resourceDenied(layer, record, causeRecord);
2513
+ return {
2514
+ name: stringField(record.name) ?? "Error",
2515
+ message,
2516
+ layer: deniedByResource ? "resource-server" : layer,
2517
+ ...stringField(record.code) ? { code: stringField(record.code) } : {},
2518
+ ...phase ? { phase } : {},
2519
+ ...combinedDetails ? { details: combinedDetails } : {}
2520
+ };
2521
+ }
2522
+ function createMcpFnTargetSuiteJUnit(report, options = {}) {
2523
+ const maxBytes = options.maxBytes ?? 1048576;
2524
+ validateArtifactCap(maxBytes);
2525
+ const safe = (0, import_oauth_core4.redactOAuthValue)(report, {
2526
+ maxDepth: 10,
2527
+ maxArrayEntries: 1e3,
2528
+ maxObjectEntries: 200,
2529
+ maxStringLength: 4096
2530
+ });
2531
+ const results = safe.results.filter((result) => result && typeof result === "object" && typeof result.name === "string");
2532
+ const omitted = report.results.length - results.length;
2533
+ const cases = results.map((result) => {
2534
+ const duration = Math.max(0, result.durationMs ?? 0) / 1e3;
2535
+ let failure = "";
2536
+ if (result.status !== "passed") {
2537
+ const failureType = result.status === "failed" ? "scenario" : "incomplete";
2538
+ failure = junitFailure(result.error ?? `Scenario ${result.status}`, failureType);
2539
+ }
2540
+ return ` <testcase name="${xml(result.name)}" classname="mcpfn.scenario" time="${duration.toFixed(3)}">${failure}</testcase>`;
2541
+ });
2542
+ if (safe.failure) {
2543
+ cases.unshift(
2544
+ ` <testcase name="${xml(safe.failure.phase ?? "target")}" classname="mcpfn.${xml(safe.failure.layer)}" time="0.000">${junitFailure(safe.failure.message, safe.failure.code ?? safe.failure.layer)}</testcase>`
2545
+ );
2546
+ }
2547
+ if (safe.status === "incomplete" || omitted > 0 || !safe.ok && !safe.failure && results.every((result) => result.status === "passed")) {
2548
+ cases.push(` <testcase name="suite-incomplete" classname="mcpfn.report" time="0.000">${junitFailure(safe.incompleteReason ?? "Suite evidence is incomplete or omitted", "incomplete")}</testcase>`);
2549
+ }
2550
+ if (cases.length === 0) {
2551
+ cases.push(
2552
+ ' <testcase name="target-suite" classname="mcpfn.target" time="0.000"></testcase>'
2553
+ );
2554
+ }
2555
+ const failures = cases.filter((entry) => entry.includes("<failure ")).length;
2556
+ let serialized = junitDocument(safe, cases, failures);
2557
+ if (bytes(serialized) <= maxBytes) {
2558
+ return preserveMcpFnTargetSuiteArtifact(report, serialized);
2559
+ }
2560
+ const boundedCases = [
2561
+ ` <testcase name="artifact-cap" classname="mcpfn.report" time="0.000">${junitFailure("JUnit content exceeded maxBytes and was truncated", "incomplete")}</testcase>`
2562
+ ];
2563
+ serialized = junitDocument(safe, boundedCases, 1, true);
2564
+ if (bytes(serialized) > maxBytes) {
2565
+ throw new Error("The minimum McpFn JUnit report exceeds maxBytes");
2566
+ }
2567
+ return preserveMcpFnTargetSuiteArtifact(report, serialized);
2568
+ }
2569
+ function junitDocument(report, cases, failures, truncated = false) {
2570
+ const suiteName = String((0, import_oauth_core4.redactOAuthValue)(`mcpfn:${report.target.kind}`));
2571
+ return [
2572
+ '<?xml version="1.0" encoding="UTF-8"?>',
2573
+ `<testsuite name="${xml(suiteName)}" tests="${cases.length}" failures="${failures}" errors="0" skipped="0">`,
2574
+ " <properties>",
2575
+ ` <property name="mcpfn.report.schema" value="${xml(MCPFN_REPORT_SCHEMA_VERSION)}"/>`,
2576
+ ` <property name="mcpfn.testing.version" value="${xml(MCPFN_TESTING_VERSION)}"/>`,
2577
+ ` <property name="node.version" value="${xml(report.runtime.node)}"/>`,
2578
+ ` <property name="artifact.truncated" value="${truncated}"/>`,
2579
+ " </properties>",
2580
+ ...cases,
2581
+ "</testsuite>",
2582
+ ""
2583
+ ].join("\n");
2584
+ }
2585
+ function junitFailure(message, type) {
2586
+ const safe = String((0, import_oauth_core4.redactOAuthValue)(message, { maxStringLength: 4096 }));
2587
+ return `<failure type="${xml(type)}" message="${xml(safe)}">${xml(safe)}</failure>`;
2588
+ }
2589
+ function failureLayer(phase) {
2590
+ if (phase === "resource-discovery" || phase === "transport-connect") {
2591
+ return "resource-server";
2592
+ }
2593
+ if (phase === "authorization-server-discovery" || phase === "client-registration" || phase === "authorization-request" || phase === "authorization-callback" || phase === "token-exchange" || phase === "token-refresh" || phase === "token-revocation") {
2594
+ return "authorization-server";
2595
+ }
2596
+ if (phase === "mcp-initialize" || phase === "capability-operation") {
2597
+ return "mcp-initialization";
2598
+ }
2599
+ if (phase === "scenario") return "scenario";
2600
+ if (phase === "upstream-conformance") return "upstream-conformance";
2601
+ return "mcpfn-preflight";
2602
+ }
2603
+ function stringField(value) {
2604
+ if (typeof value === "string") return value;
2605
+ if (typeof value === "number") return String(value);
2606
+ return void 0;
2607
+ }
2608
+ function xml(value) {
2609
+ return stripInvalidXmlControls(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
2610
+ }
2611
+ function stripInvalidXmlControls(value) {
2612
+ return value.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F\uFFFE\uFFFF\uD800-\uDFFF]/gu, "");
2613
+ }
2614
+ function validateArtifactCap(maxBytes) {
2615
+ if (!Number.isInteger(maxBytes) || maxBytes < 1024) {
2616
+ throw new Error("maxBytes must be an integer of at least 1024");
2617
+ }
2618
+ }
2619
+ function bytes(value) {
2620
+ return new TextEncoder().encode(value).byteLength;
2621
+ }
2622
+
2623
+ // src/conformance.ts
1000
2624
  var OFFICIAL_CONFORMANCE_VERSION = "0.1.16";
1001
- async function createAuthenticatedConformanceProxy(options) {
1002
- const upstream = new URL(options.url);
2625
+ function validateAuthenticatedConformanceUrl(url) {
2626
+ const upstream = new URL(url);
1003
2627
  if (!["http:", "https:"].includes(upstream.protocol)) {
1004
2628
  throw new TypeError(
1005
2629
  "Authenticated conformance upstream must use HTTP or HTTPS"
@@ -1016,10 +2640,14 @@ async function createAuthenticatedConformanceProxy(options) {
1016
2640
  "Authenticated conformance upstream must use a literal loopback address"
1017
2641
  );
1018
2642
  }
2643
+ return { upstream, hostname };
2644
+ }
2645
+ async function createAuthenticatedConformanceProxy(options) {
2646
+ const { upstream, hostname } = validateAuthenticatedConformanceUrl(options.url);
1019
2647
  const protocol = upstream.protocol === "https:" ? "https:" : "http:";
1020
2648
  const port = upstream.port === "" ? void 0 : Number(upstream.port);
1021
2649
  const requestPath = `${upstream.pathname}${upstream.search}`;
1022
- const injected = new Headers(options.headers);
2650
+ const injected = validateRemoteCredentialHeaders(options.headers);
1023
2651
  const activeRequests = /* @__PURE__ */ new Set();
1024
2652
  const activeSockets = /* @__PURE__ */ new Set();
1025
2653
  let proxyAuthority;
@@ -1082,20 +2710,25 @@ async function createAuthenticatedConformanceProxy(options) {
1082
2710
  `http://127.0.0.1:${address.port}`
1083
2711
  );
1084
2712
  proxyAuthority = url.host;
2713
+ let closePromise;
1085
2714
  return {
1086
2715
  url: url.toString(),
1087
- close: async () => {
1088
- for (const request of activeRequests) request.destroy();
1089
- for (const socket of activeSockets) socket.destroy();
1090
- if (!server.listening) return;
1091
- await new Promise((resolve, reject) => {
1092
- server.close((error) => error ? reject(error) : resolve());
1093
- });
2716
+ close: () => {
2717
+ closePromise ??= (async () => {
2718
+ for (const request of activeRequests) request.destroy();
2719
+ for (const socket of activeSockets) socket.destroy();
2720
+ if (!server.listening) return;
2721
+ await new Promise((resolve, reject) => {
2722
+ server.close((error) => error ? reject(error) : resolve());
2723
+ });
2724
+ })();
2725
+ return closePromise;
1094
2726
  }
1095
2727
  };
1096
2728
  }
1097
2729
  function normalizeLoopbackHostname(hostname) {
1098
- if (hostname === "127.0.0.1") return "127.0.0.1";
2730
+ const parts = hostname.split(".");
2731
+ if (parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255)) return hostname;
1099
2732
  if (hostname === "[::1]") return "::1";
1100
2733
  return void 0;
1101
2734
  }
@@ -1103,9 +2736,9 @@ function npxInvocation(args) {
1103
2736
  if (process.platform !== "win32") return { command: "npx", args };
1104
2737
  const npmExecPath = process.env.npm_execpath;
1105
2738
  const candidates = [
1106
- npmExecPath ? import_node_path.default.join(import_node_path.default.dirname(npmExecPath), "npx-cli.js") : void 0,
1107
- import_node_path.default.resolve(import_node_path.default.dirname(process.execPath), "node_modules/npm/bin/npx-cli.js"),
1108
- import_node_path.default.resolve(import_node_path.default.dirname(process.execPath), "../lib/node_modules/npm/bin/npx-cli.js")
2739
+ npmExecPath ? import_node_path2.default.join(import_node_path2.default.dirname(npmExecPath), "npx-cli.js") : void 0,
2740
+ import_node_path2.default.resolve(import_node_path2.default.dirname(process.execPath), "node_modules/npm/bin/npx-cli.js"),
2741
+ import_node_path2.default.resolve(import_node_path2.default.dirname(process.execPath), "../lib/node_modules/npm/bin/npx-cli.js")
1109
2742
  ].filter((candidate) => Boolean(candidate));
1110
2743
  const npxCli = candidates.find((candidate) => (0, import_node_fs.existsSync)(candidate));
1111
2744
  if (!npxCli) {
@@ -1131,7 +2764,19 @@ function buildOfficialConformanceArgs(options) {
1131
2764
  if (options.verbose) args.push("--verbose");
1132
2765
  return args;
1133
2766
  }
2767
+ function buildOfficialConformanceEnvironment(sensitiveNames = [], source = process.env) {
2768
+ const environment = { ...source };
2769
+ const sensitive = new Set(sensitiveNames.map((name) => name.toLowerCase()));
2770
+ for (const name of Object.keys(environment)) {
2771
+ if (sensitive.has(name.toLowerCase())) delete environment[name];
2772
+ }
2773
+ environment.PATH = [import_node_path2.default.dirname(process.execPath), environment.PATH].filter(Boolean).join(import_node_path2.default.delimiter);
2774
+ return environment;
2775
+ }
1134
2776
  async function runOfficialConformance(options) {
2777
+ return runConformance(options, (value) => String((0, import_oauth_core5.redactOAuthValue)(value, { maxStringLength: 262144 })));
2778
+ }
2779
+ async function runConformance(options, redactOutput) {
1135
2780
  const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
1136
2781
  if (nodeMajor < 22) {
1137
2782
  throw new Error(
@@ -1140,40 +2785,170 @@ async function runOfficialConformance(options) {
1140
2785
  }
1141
2786
  const args = buildOfficialConformanceArgs(options);
1142
2787
  const invocation = npxInvocation(args);
1143
- return await new Promise((resolve, reject) => {
1144
- const child = (0, import_node_child_process.spawn)(invocation.command, invocation.args, {
2788
+ const childEnvironment = buildOfficialConformanceEnvironment(
2789
+ options.sensitiveEnvironmentVariables
2790
+ );
2791
+ return await new Promise((resolve) => {
2792
+ const child = (0, import_node_child_process2.spawn)(invocation.command, invocation.args, {
2793
+ detached: process.platform !== "win32",
1145
2794
  cwd: options.cwd,
1146
- env: {
1147
- ...process.env,
1148
- PATH: [import_node_path.default.dirname(process.execPath), process.env.PATH].filter(Boolean).join(import_node_path.default.delimiter)
1149
- },
2795
+ env: childEnvironment,
1150
2796
  stdio: options.stdio === "inherit" ? "inherit" : "pipe"
1151
2797
  });
1152
2798
  let stdout = "";
1153
2799
  let stderr = "";
1154
- child.stdout?.on("data", (chunk) => {
1155
- stdout += chunk.toString();
1156
- });
1157
- child.stderr?.on("data", (chunk) => {
1158
- stderr += chunk.toString();
1159
- });
1160
- child.once("error", reject);
1161
- child.once("close", (code) => {
1162
- resolve({ exitCode: code ?? 1, stdout, stderr });
2800
+ let capturedBytes = 0;
2801
+ let outputExceeded = false;
2802
+ const capture = (chunk, stream) => {
2803
+ if (outputExceeded) return;
2804
+ capturedBytes += Buffer.byteLength(chunk, "utf8");
2805
+ if (capturedBytes > 262144) {
2806
+ outputExceeded = true;
2807
+ stdout = "";
2808
+ stderr = "";
2809
+ void terminateConformanceRunner(child).finally(() => finish(1));
2810
+ return;
2811
+ }
2812
+ if (stream === "stdout") stdout += chunk.toString();
2813
+ else stderr += chunk.toString();
2814
+ };
2815
+ child.stdout?.setEncoding("utf8");
2816
+ child.stderr?.setEncoding("utf8");
2817
+ child.stdout?.on("data", (chunk) => capture(chunk, "stdout"));
2818
+ child.stderr?.on("data", (chunk) => capture(chunk, "stderr"));
2819
+ let settled = false;
2820
+ child.once("error", (error) => {
2821
+ if (settled) return;
2822
+ settled = true;
2823
+ const failure = normalizeMcpFnReportFailure(error, "upstream-conformance");
2824
+ resolve({
2825
+ formatVersion: 1,
2826
+ kind: "mcpfn.official-conformance-report",
2827
+ ok: false,
2828
+ suiteVersion: OFFICIAL_CONFORMANCE_VERSION,
2829
+ exitCode: 1,
2830
+ stdout: "",
2831
+ stderr: failure.message,
2832
+ failure
2833
+ });
1163
2834
  });
2835
+ const finish = (code) => {
2836
+ if (settled) return;
2837
+ settled = true;
2838
+ let safeStdout = "";
2839
+ let safeStderr = "";
2840
+ let outputFailure = false;
2841
+ try {
2842
+ if (outputExceeded) throw new Error("capture limit");
2843
+ safeStdout = redactOutput(stdout);
2844
+ safeStderr = redactOutput(stderr);
2845
+ } catch {
2846
+ outputFailure = true;
2847
+ safeStdout = "";
2848
+ safeStderr = outputExceeded ? "Conformance output exceeded its capture limit" : "Conformance output omitted because credential redaction exceeded its bounds";
2849
+ }
2850
+ const exitCode = outputFailure ? 1 : code ?? 1;
2851
+ const failure = exitCode === 0 ? void 0 : normalizeMcpFnReportFailure(
2852
+ new Error(safeStderr || safeStdout || `Official conformance exited ${exitCode}`),
2853
+ "upstream-conformance"
2854
+ );
2855
+ resolve({
2856
+ formatVersion: 1,
2857
+ kind: "mcpfn.official-conformance-report",
2858
+ ok: exitCode === 0,
2859
+ suiteVersion: OFFICIAL_CONFORMANCE_VERSION,
2860
+ exitCode,
2861
+ stdout: safeStdout,
2862
+ stderr: safeStderr,
2863
+ ...failure ? { failure } : {}
2864
+ });
2865
+ };
2866
+ child.once("close", finish);
1164
2867
  });
1165
2868
  }
2869
+ var retainedConformanceCleanup = /* @__PURE__ */ new WeakMap();
2870
+ var McpFnConformanceCleanupError = class extends Error {
2871
+ constructor(release, result) {
2872
+ super("Authenticated conformance credential cleanup failed; retry cleanup");
2873
+ this.result = result;
2874
+ this.name = "McpFnConformanceCleanupError";
2875
+ retainedConformanceCleanup.set(this, release);
2876
+ }
2877
+ result;
2878
+ async retryCleanup() {
2879
+ const release = retainedConformanceCleanup.get(this);
2880
+ if (!release) return;
2881
+ try {
2882
+ await release();
2883
+ } catch {
2884
+ throw this;
2885
+ }
2886
+ retainedConformanceCleanup.delete(this);
2887
+ }
2888
+ };
1166
2889
  async function runAuthenticatedOfficialConformance(options) {
1167
- const { headers, ...conformance } = options;
1168
- const proxy = await createAuthenticatedConformanceProxy({
1169
- url: conformance.url,
1170
- headers
1171
- });
2890
+ if (options.outputDir !== void 0) throw new TypeError("Authenticated conformance does not support outputDir; serialize the redacted result instead");
2891
+ const { headers, credential, ...conformance } = options;
2892
+ if (headers === void 0 === (credential === void 0)) {
2893
+ throw new TypeError("Provide exactly one of credential or headers for authenticated conformance");
2894
+ }
2895
+ validateAuthenticatedConformanceUrl(conformance.url);
2896
+ const lease = await acquireRemoteCredential(
2897
+ credential ?? { headers },
2898
+ {
2899
+ url: conformance.url,
2900
+ requestId: (0, import_node_crypto3.randomUUID)()
2901
+ }
2902
+ );
2903
+ let proxy;
2904
+ let result;
2905
+ let operationFailure;
2906
+ const safeFailure = (error) => {
2907
+ let message = "Authenticated conformance operation failed";
2908
+ try {
2909
+ message = redactRemoteCredential(lease.credential, error instanceof Error ? error.message : String(error));
2910
+ } catch {
2911
+ }
2912
+ return { error: new Error(message) };
2913
+ };
1172
2914
  try {
1173
- return await runOfficialConformance({ ...conformance, url: proxy.url });
1174
- } finally {
1175
- await proxy.close();
2915
+ proxy = await createAuthenticatedConformanceProxy({
2916
+ url: conformance.url,
2917
+ headers: lease.credential.headers
2918
+ });
2919
+ result = redactRemoteCredential(lease.credential, await runConformance({ ...conformance, stdio: "pipe", url: proxy.url }, (value) => String(redactRemoteCredential(lease.credential, value))), { preserveKeys: true });
2920
+ } catch (error) {
2921
+ operationFailure = safeFailure(error);
2922
+ }
2923
+ try {
2924
+ await proxy?.close();
2925
+ } catch (error) {
2926
+ operationFailure ??= safeFailure(error);
1176
2927
  }
2928
+ let released = false;
2929
+ for (let attempt = 0; attempt < 3; attempt++) {
2930
+ try {
2931
+ await lease.release();
2932
+ released = true;
2933
+ break;
2934
+ } catch {
2935
+ }
2936
+ }
2937
+ if (!released) {
2938
+ const message = "Authenticated conformance credential cleanup failed; retry cleanup";
2939
+ const cleanupFailure = normalizeMcpFnReportFailure(new Error(message), lease.cleanupPhase());
2940
+ const failedResult = result ? {
2941
+ ...result,
2942
+ ok: false,
2943
+ exitCode: result.exitCode || 1,
2944
+ cleanupFailure,
2945
+ ...result.failure ? {} : { failure: cleanupFailure }
2946
+ } : void 0;
2947
+ throw new McpFnConformanceCleanupError(() => lease.release(), failedResult);
2948
+ }
2949
+ if (operationFailure) throw operationFailure.error;
2950
+ if (!result) throw new Error("Conformance produced no result");
2951
+ return result;
1177
2952
  }
1178
2953
 
1179
2954
  // src/host-profiles.ts
@@ -1258,37 +3033,405 @@ function checkHostCompatibility(manifest, profile) {
1258
3033
  path: `extensions.${extension}`,
1259
3034
  message: `${profile.name} does not support ${extension}`
1260
3035
  });
1261
- }
1262
- if (!extensions.has(import_core.MCP_APP_EXTENSION_ID)) {
1263
- for (const tool of manifest.tools) {
1264
- const ui = tool.metadata?.ui;
1265
- if (ui?.visibility?.length && !ui.visibility.includes("model")) {
1266
- issues.push({
1267
- severity: "incompatible",
1268
- code: "app-only-tool-unavailable",
1269
- path: `tools.${tool.name}.metadata.ui.visibility`,
1270
- message: `${tool.name} is app-only but ${profile.name} does not support MCP Apps`
1271
- });
1272
- }
3036
+ }
3037
+ if (!extensions.has(import_core.MCP_APP_EXTENSION_ID)) {
3038
+ for (const tool of manifest.tools) {
3039
+ const ui = tool.metadata?.ui;
3040
+ if (ui?.visibility?.length && !ui.visibility.includes("model")) {
3041
+ issues.push({
3042
+ severity: "incompatible",
3043
+ code: "app-only-tool-unavailable",
3044
+ path: `tools.${tool.name}.metadata.ui.visibility`,
3045
+ message: `${tool.name} is app-only but ${profile.name} does not support MCP Apps`
3046
+ });
3047
+ }
3048
+ }
3049
+ }
3050
+ issues.sort((left, right) => {
3051
+ if (left.path !== right.path) return left.path < right.path ? -1 : 1;
3052
+ if (left.code < right.code) return -1;
3053
+ if (left.code > right.code) return 1;
3054
+ return 0;
3055
+ });
3056
+ let status = "compatible";
3057
+ if (issues.some((issue) => issue.severity === "incompatible")) {
3058
+ status = "incompatible";
3059
+ } else if (issues.length > 0) {
3060
+ status = "degraded";
3061
+ }
3062
+ return { status, compatible: status !== "incompatible", issues };
3063
+ }
3064
+
3065
+ // src/host-authorization.ts
3066
+ var import_oauth_core6 = require("@superfunctions/oauth-core");
3067
+ function createHostedAuthorizationFixtures(options) {
3068
+ const issuer = new URL(options.issuer);
3069
+ const resource = new URL(options.resource).toString();
3070
+ const chatgptRedirect = options.chatgptRedirectUri ?? "https://chatgpt.com/connector_platform_oauth_redirect";
3071
+ const claudeRedirect = options.claudeRedirectUri ?? "https://claude.example.com/oauth/callback";
3072
+ const verifier = "mcpfn-role3-shared-pkce-verifier-000000000000000000000000";
3073
+ const registration = (clientId, source, redirectUri, grantTypes) => ({
3074
+ clientId,
3075
+ source,
3076
+ metadata: {
3077
+ client_name: `McpFn ${source} fixture`,
3078
+ redirect_uris: [redirectUri],
3079
+ response_types: ["code"],
3080
+ grant_types: grantTypes,
3081
+ token_endpoint_auth_method: "none"
3082
+ }
3083
+ });
3084
+ const authorization = (clientId, redirectUri) => ({
3085
+ clientId,
3086
+ redirectUri,
3087
+ resource,
3088
+ scopes: ["mcp:read"],
3089
+ state: "mcpfn-role3-state",
3090
+ codeVerifier: verifier
3091
+ });
3092
+ const chatgptId = "chatgpt-client";
3093
+ const claudeId = new URL("/client-metadata/claude", issuer).toString();
3094
+ const dcrId = "dcr-client";
3095
+ const supported = ["authorization_code", "refresh_token"];
3096
+ return [
3097
+ {
3098
+ id: "chatgpt-pre-registered-code-pkce",
3099
+ host: "chatgpt",
3100
+ registration: registration(chatgptId, "pre-registered", chatgptRedirect, supported),
3101
+ authorization: authorization(chatgptId, chatgptRedirect),
3102
+ token: {
3103
+ grantType: "authorization_code",
3104
+ refreshAfterExchange: true
3105
+ },
3106
+ expected: { outcome: "allowed" }
3107
+ },
3108
+ {
3109
+ id: "claude-client-metadata-extensible-grants",
3110
+ host: "claude",
3111
+ registration: registration(claudeId, "client-metadata-document", claudeRedirect, [
3112
+ ...supported,
3113
+ "urn:ietf:params:oauth:grant-type:jwt-bearer",
3114
+ "urn:ietf:params:oauth:grant-type:device_code",
3115
+ "urn:example:params:oauth:grant-type:extension"
3116
+ ]),
3117
+ authorization: authorization(claudeId, claudeRedirect),
3118
+ token: {
3119
+ grantType: "authorization_code",
3120
+ refreshAfterExchange: true
3121
+ },
3122
+ expected: { outcome: "allowed" }
3123
+ },
3124
+ {
3125
+ id: "dynamic-registration-code-pkce",
3126
+ host: "generic",
3127
+ registration: registration(dcrId, "dynamic", "http://127.0.0.1/callback", supported),
3128
+ authorization: authorization(dcrId, "http://127.0.0.1/callback"),
3129
+ token: { grantType: "authorization_code" },
3130
+ expected: { outcome: "allowed" }
3131
+ },
3132
+ {
3133
+ id: "chatgpt-unregistered-redirect",
3134
+ host: "chatgpt",
3135
+ registration: registration(chatgptId, "pre-registered", chatgptRedirect, supported),
3136
+ authorization: authorization(chatgptId, `${chatgptRedirect}/drift`),
3137
+ expected: {
3138
+ outcome: "rejected",
3139
+ errorCode: "invalid_request",
3140
+ phase: "authorization-request"
3141
+ }
3142
+ },
3143
+ {
3144
+ id: "incompatible-registration-grants",
3145
+ host: "generic",
3146
+ registration: registration(
3147
+ "incompatible-client",
3148
+ "pre-registered",
3149
+ "https://client.example.com/callback",
3150
+ ["urn:ietf:params:oauth:grant-type:jwt-bearer"]
3151
+ ),
3152
+ authorization: authorization(
3153
+ "incompatible-client",
3154
+ "https://client.example.com/callback"
3155
+ ),
3156
+ expected: {
3157
+ outcome: "rejected",
3158
+ errorCode: "invalid_client",
3159
+ phase: "authorization-request"
3160
+ }
3161
+ },
3162
+ {
3163
+ id: "actual-unsupported-token-grant",
3164
+ host: "generic",
3165
+ registration: registration(
3166
+ "unsupported-token-client",
3167
+ "pre-registered",
3168
+ "https://client.example.com/callback",
3169
+ supported
3170
+ ),
3171
+ authorization: authorization(
3172
+ "unsupported-token-client",
3173
+ "https://client.example.com/callback"
3174
+ ),
3175
+ token: { grantType: "urn:example:unsupported-token-grant" },
3176
+ expected: {
3177
+ outcome: "rejected",
3178
+ errorCode: "unsupported_grant_type",
3179
+ phase: "token-exchange"
3180
+ }
3181
+ }
3182
+ ];
3183
+ }
3184
+ async function runHostedAuthorizationRegression(target, fixtures) {
3185
+ const results = [];
3186
+ for (const fixture of fixtures) {
3187
+ results.push(await runHostedCase(target, fixture));
3188
+ }
3189
+ return results;
3190
+ }
3191
+ async function runHostedCase(target, fixture) {
3192
+ let phase = "client-registration";
3193
+ let responseStatus;
3194
+ try {
3195
+ await target.prepareRegistration(structuredClone(fixture.registration));
3196
+ phase = "authorization-request";
3197
+ const authorization = hostedAuthorizationUrl(target.issuer, fixture);
3198
+ responseStatus = void 0;
3199
+ const authorizationResponse = await target.request(new Request(authorization, {
3200
+ redirect: "manual"
3201
+ }));
3202
+ responseStatus = authorizationResponse.status;
3203
+ const authorizationError = await oauthError2(authorizationResponse);
3204
+ if (authorizationError) {
3205
+ await validateOAuthRejection(authorizationResponse, fixture, target.issuer, "authorization");
3206
+ return assessHostedCase(fixture, phase, authorizationResponse.status, authorizationError);
3207
+ }
3208
+ if (!isRedirect(authorizationResponse.status)) {
3209
+ throw new Error(`Authorization request did not redirect (HTTP ${authorizationResponse.status})`);
3210
+ }
3211
+ const code = validatedRedirectCode(authorizationResponse, fixture, target.issuer);
3212
+ if (fixture.token) return runHostedTokenCase(target, fixture, fixture.token, code);
3213
+ return assessHostedCase(fixture, phase, responseStatus);
3214
+ } catch (error) {
3215
+ return assessHostedFailure(fixture, phase, responseStatus, error);
3216
+ }
3217
+ }
3218
+ async function runHostedTokenCase(target, fixture, token, code) {
3219
+ let phase = token.grantType === "refresh_token" ? "token-refresh" : "token-exchange";
3220
+ let responseStatus;
3221
+ try {
3222
+ const tokenResponse = await target.request(hostedTokenRequest(
3223
+ target.issuer,
3224
+ tokenRequestBody(fixture, token, code)
3225
+ ));
3226
+ responseStatus = tokenResponse.status;
3227
+ const tokenError = await oauthError2(tokenResponse);
3228
+ if (tokenError) {
3229
+ await validateOAuthRejection(tokenResponse, fixture, target.issuer, "token");
3230
+ return assessHostedCase(fixture, phase, responseStatus, tokenError);
3231
+ }
3232
+ if (!tokenResponse.ok) throw new Error(`Token request returned HTTP ${tokenResponse.status}`);
3233
+ const tokenSet = await validatedTokenSet(tokenResponse);
3234
+ if (!token.refreshAfterExchange) {
3235
+ return assessHostedCase(fixture, phase, responseStatus);
3236
+ }
3237
+ if (typeof tokenSet.refresh_token !== "string" || !tokenSet.refresh_token) {
3238
+ throw new Error("Authorization-code response did not include a refresh token");
3239
+ }
3240
+ phase = "token-refresh";
3241
+ responseStatus = void 0;
3242
+ const refreshBody = new URLSearchParams({
3243
+ grant_type: "refresh_token",
3244
+ client_id: fixture.authorization.clientId,
3245
+ refresh_token: tokenSet.refresh_token,
3246
+ resource: fixture.authorization.resource
3247
+ });
3248
+ const refreshResponse = await target.request(hostedTokenRequest(target.issuer, refreshBody));
3249
+ responseStatus = refreshResponse.status;
3250
+ const refreshError = await oauthError2(refreshResponse);
3251
+ if (refreshError) {
3252
+ await validateOAuthRejection(refreshResponse, fixture, target.issuer, "token");
3253
+ return assessHostedCase(fixture, phase, responseStatus, refreshError);
3254
+ }
3255
+ if (!refreshResponse.ok) {
3256
+ throw new Error(`Refresh request returned HTTP ${refreshResponse.status}`);
1273
3257
  }
3258
+ await validatedTokenSet(refreshResponse);
3259
+ return assessHostedCase(fixture, phase, responseStatus);
3260
+ } catch (error) {
3261
+ return assessHostedFailure(fixture, phase, responseStatus, error);
1274
3262
  }
1275
- issues.sort((left, right) => {
1276
- if (left.path !== right.path) return left.path < right.path ? -1 : 1;
1277
- if (left.code < right.code) return -1;
1278
- if (left.code > right.code) return 1;
1279
- return 0;
3263
+ }
3264
+ function tokenRequestBody(fixture, token, code) {
3265
+ const body = new URLSearchParams({
3266
+ grant_type: token.grantType,
3267
+ client_id: fixture.authorization.clientId
1280
3268
  });
1281
- let status = "compatible";
1282
- if (issues.some((issue) => issue.severity === "incompatible")) {
1283
- status = "incompatible";
1284
- } else if (issues.length > 0) {
1285
- status = "degraded";
3269
+ if (token.grantType === "authorization_code") {
3270
+ body.set("code", token.code ?? code);
3271
+ body.set("redirect_uri", fixture.authorization.redirectUri);
3272
+ body.set("code_verifier", fixture.authorization.codeVerifier);
3273
+ body.set("resource", fixture.authorization.resource);
3274
+ } else if (token.refreshToken) {
3275
+ body.set("refresh_token", token.refreshToken);
3276
+ body.set("resource", fixture.authorization.resource);
3277
+ }
3278
+ return body;
3279
+ }
3280
+ function hostedTokenRequest(issuer, body) {
3281
+ return new Request(new URL("token", ensureTrailingSlash(issuer)), {
3282
+ method: "POST",
3283
+ redirect: "manual",
3284
+ headers: { "content-type": "application/x-www-form-urlencoded" },
3285
+ body
3286
+ });
3287
+ }
3288
+ function assessHostedFailure(fixture, phase, responseStatus, error) {
3289
+ const redacted = (0, import_oauth_core6.redactOAuthValue)(error);
3290
+ const safe = redacted && typeof redacted === "object" ? redacted : {};
3291
+ const code = typeof safe.code === "string" ? safe.code : void 0;
3292
+ const message = typeof safe.message === "string" ? safe.message : String(redacted);
3293
+ return assessHostedCase(
3294
+ fixture,
3295
+ phase,
3296
+ responseStatus,
3297
+ code,
3298
+ message || "Hosted adapter failed"
3299
+ );
3300
+ }
3301
+ function hostedAuthorizationUrl(issuer, fixture) {
3302
+ const authorization = new URL("authorize", ensureTrailingSlash(issuer));
3303
+ authorization.searchParams.set("response_type", "code");
3304
+ authorization.searchParams.set("client_id", fixture.authorization.clientId);
3305
+ authorization.searchParams.set("redirect_uri", fixture.authorization.redirectUri);
3306
+ authorization.searchParams.set(
3307
+ "code_challenge",
3308
+ (0, import_oauth_core6.derivePkceS256Challenge)(fixture.authorization.codeVerifier)
3309
+ );
3310
+ authorization.searchParams.set("code_challenge_method", "S256");
3311
+ authorization.searchParams.set("state", fixture.authorization.state);
3312
+ authorization.searchParams.set("resource", fixture.authorization.resource);
3313
+ authorization.searchParams.set("scope", fixture.authorization.scopes.join(" "));
3314
+ return authorization;
3315
+ }
3316
+ function assessHostedCase(fixture, phase, responseStatus, errorCode, error) {
3317
+ const observedOutcome = errorCode || error ? "rejected" : "allowed";
3318
+ const expected = fixture.expected;
3319
+ const passed = !error && observedOutcome === expected.outcome && (expected.errorCode === void 0 || expected.errorCode === errorCode) && (expected.phase === void 0 || expected.phase === phase);
3320
+ const expectedCode = expected.errorCode ? `:${expected.errorCode}` : "";
3321
+ const observedCode = errorCode ? `:${errorCode}` : "";
3322
+ const mismatch = `Expected ${expected.outcome}${expectedCode}, received ${observedOutcome}${observedCode}`;
3323
+ return {
3324
+ formatVersion: 1,
3325
+ id: fixture.id,
3326
+ host: fixture.host,
3327
+ status: passed ? "passed" : "failed",
3328
+ phase,
3329
+ layer: phase === "client-registration" ? "mcpfn-preflight" : "authorization-server",
3330
+ ...responseStatus === void 0 ? {} : { responseStatus },
3331
+ ...errorCode ? { errorCode } : {},
3332
+ ...!passed || error ? {
3333
+ error: error ?? mismatch
3334
+ } : {}
3335
+ };
3336
+ }
3337
+ function isJsonResponse(response) {
3338
+ const mediaType = (response.headers.get("content-type") ?? "").split(";", 1)[0].trim().toLowerCase();
3339
+ return mediaType === "application/json" || /^application\/[a-z0-9!#$&^_.+-]+\+json$/.test(mediaType);
3340
+ }
3341
+ async function oauthError2(response) {
3342
+ const location = response.headers.get("location");
3343
+ if (location) {
3344
+ const code = new URL(location).searchParams.get("error");
3345
+ if (code) return code;
3346
+ }
3347
+ if (!isJsonResponse(response)) return void 0;
3348
+ const body = await response.clone().json().catch(() => void 0);
3349
+ return typeof body?.error === "string" ? body.error : void 0;
3350
+ }
3351
+ async function validateOAuthRejection(response, fixture, issuer, context) {
3352
+ if (context === "authorization" && isRedirect(response.status)) {
3353
+ validateAuthorizationErrorRedirect(response, fixture, issuer);
3354
+ return;
1286
3355
  }
1287
- return { status, compatible: status !== "incompatible", issues };
3356
+ const body = await response.clone().json().catch(() => void 0);
3357
+ const registered = fixture.registration.metadata.redirect_uris?.includes(fixture.authorization.redirectUri);
3358
+ if (context === "authorization" && body?.error !== "invalid_client" && registered) {
3359
+ throw new Error("Registered authorization requests require an OAuth error callback");
3360
+ }
3361
+ if (response.status !== 400) throw new Error("OAuth error response has invalid HTTP status");
3362
+ if (response.headers.has("location") || !isJsonResponse(response) || !body || typeof body.error !== "string" || !body.error) {
3363
+ throw new Error("OAuth error response must be a JSON error envelope");
3364
+ }
3365
+ }
3366
+ function validateAuthorizationErrorRedirect(response, fixture, issuer) {
3367
+ const callback = new URL(response.headers.get("location") ?? "");
3368
+ const registered = fixture.registration.metadata.redirect_uris;
3369
+ if (!registered?.includes(fixture.authorization.redirectUri)) {
3370
+ throw new Error("Rejected redirect URI must not receive a callback");
3371
+ }
3372
+ const errors = callback.searchParams.getAll("error");
3373
+ if (errors.length !== 1 || !errors[0] || callback.searchParams.has("code")) {
3374
+ throw new Error("Invalid OAuth error callback");
3375
+ }
3376
+ if (errors[0] === "invalid_client") {
3377
+ throw new Error("Invalid-client authorization failures must not redirect");
3378
+ }
3379
+ callback.searchParams.delete("error");
3380
+ callback.searchParams.delete("error_description");
3381
+ callback.searchParams.delete("error_uri");
3382
+ callback.searchParams.set("code", "error-envelope-validation");
3383
+ validatedRedirectCode(
3384
+ new Response(null, { status: response.status, headers: { location: callback.toString() } }),
3385
+ fixture,
3386
+ issuer
3387
+ );
3388
+ }
3389
+ function validatedRedirectCode(response, fixture, issuer) {
3390
+ const location = response.headers.get("location");
3391
+ if (!location) throw new Error("Authorization callback is missing");
3392
+ const callback = new URL(location);
3393
+ const expected = new URL(fixture.authorization.redirectUri);
3394
+ const codes = callback.searchParams.getAll("code");
3395
+ const states = callback.searchParams.getAll("state");
3396
+ if (codes.length !== 1 || !codes[0] || states.length !== 1 || states[0] !== fixture.authorization.state) {
3397
+ throw new Error("Authorization callback code or state is invalid");
3398
+ }
3399
+ const issuers = callback.searchParams.getAll("iss");
3400
+ if (issuers.length > 1 || issuers.length === 1 && issuers[0] !== issuer) {
3401
+ throw new Error("Authorization callback issuer is invalid");
3402
+ }
3403
+ if (callback.protocol !== expected.protocol || callback.host !== expected.host || callback.username !== expected.username || callback.password !== expected.password || callback.pathname !== expected.pathname || callback.hash !== expected.hash || [...new Set(expected.searchParams.keys())].some((key) => JSON.stringify(callback.searchParams.getAll(key)) !== JSON.stringify(expected.searchParams.getAll(key)))) {
3404
+ throw new Error("Authorization callback destination is invalid");
3405
+ }
3406
+ return codes[0];
3407
+ }
3408
+ async function validatedTokenSet(response) {
3409
+ if (response.status !== 200) throw new Error("Successful token responses must use HTTP 200");
3410
+ if (!isJsonResponse(response)) throw new Error("Token response must use a JSON media type");
3411
+ if (!hasHeaderDirective(response.headers.get("cache-control"), "no-store") || !hasHeaderDirective(response.headers.get("pragma"), "no-cache")) {
3412
+ throw new Error("Token responses must disable caching with Cache-Control: no-store and Pragma: no-cache");
3413
+ }
3414
+ const value = await response.clone().json();
3415
+ if (!value || typeof value.access_token !== "string" || !/^[\dA-Za-z._~+/-]+=*$/.test(value.access_token) || typeof value.token_type !== "string" || value.token_type.toLowerCase() !== "bearer") {
3416
+ throw new Error("Token response requires an access token and Bearer token type");
3417
+ }
3418
+ return value;
3419
+ }
3420
+ function hasHeaderDirective(value, expected) {
3421
+ return (value ?? "").split(",").some(
3422
+ (directive) => directive.trim().toLowerCase() === expected
3423
+ );
3424
+ }
3425
+ function isRedirect(status) {
3426
+ return [301, 302, 303, 307, 308].includes(status);
3427
+ }
3428
+ function ensureTrailingSlash(value) {
3429
+ const url = new URL(value);
3430
+ if (!url.pathname.endsWith("/")) url.pathname += "/";
3431
+ return url;
1288
3432
  }
1289
3433
 
1290
3434
  // src/scenarios.ts
1291
- var import_oauth_core2 = require("@superfunctions/oauth-core");
1292
3435
  function createMcpFnScenario(name, operation, result) {
1293
3436
  switch (operation.kind) {
1294
3437
  case "tools.call":
@@ -1517,16 +3660,31 @@ async function runScenarios(client, scenarios, options = {}) {
1517
3660
  const observedEvents = [];
1518
3661
  const maxObservedEvents = options.maxObservedEvents ?? 500;
1519
3662
  let droppedObservedEvents = 0;
3663
+ let redactionOmittedObservedEvents = 0;
3664
+ let observedRedactionOmissionEvents = 0;
1520
3665
  let attributedDroppedObservedEvents = 0;
3666
+ let attributedRedactionOmittedObservedEvents = 0;
3667
+ const initialClientEventRedactionOmissions = client.session.getRedactionOmissionCounts().clientEvents;
1521
3668
  const pushResult = (result) => {
1522
3669
  const newlyDropped = droppedObservedEvents - attributedDroppedObservedEvents;
3670
+ const newlyRedactionOmitted = redactionOmittedObservedEvents - attributedRedactionOmittedObservedEvents;
1523
3671
  results.push({
1524
3672
  ...result,
1525
- ...newlyDropped > 0 ? { droppedObservedEvents: (result.droppedObservedEvents ?? 0) + newlyDropped } : {}
3673
+ ...newlyDropped > 0 ? { droppedObservedEvents: (result.droppedObservedEvents ?? 0) + newlyDropped } : {},
3674
+ ...newlyRedactionOmitted > 0 ? {
3675
+ redactionOmittedObservedEvents: (result.redactionOmittedObservedEvents ?? 0) + newlyRedactionOmitted
3676
+ } : {}
1526
3677
  });
1527
3678
  attributedDroppedObservedEvents = droppedObservedEvents;
3679
+ attributedRedactionOmittedObservedEvents = redactionOmittedObservedEvents;
1528
3680
  };
1529
3681
  const unsubscribe = client.session.onEvent((event) => {
3682
+ if (client.session.isRedactionOmission(event)) {
3683
+ observedRedactionOmissionEvents += 1;
3684
+ droppedObservedEvents += 1;
3685
+ redactionOmittedObservedEvents += 1;
3686
+ return;
3687
+ }
1530
3688
  observedEvents.push(event);
1531
3689
  if (observedEvents.length > maxObservedEvents) {
1532
3690
  observedEvents.shift();
@@ -1550,7 +3708,7 @@ async function runScenarios(client, scenarios, options = {}) {
1550
3708
  ...common,
1551
3709
  status: "incomplete",
1552
3710
  durationMs: 0,
1553
- error: truncateError(`Skipped after timed-out scenario: ${timedOutScenario}`, options)
3711
+ error: truncateError(`Skipped after timed-out scenario: ${timedOutScenario}`, options, client)
1554
3712
  });
1555
3713
  continue;
1556
3714
  }
@@ -1562,7 +3720,8 @@ async function runScenarios(client, scenarios, options = {}) {
1562
3720
  durationMs: 0,
1563
3721
  error: truncateError(
1564
3722
  `Missing scenario variables: ${resolved.missing.join(", ")}`,
1565
- options
3723
+ options,
3724
+ client
1566
3725
  )
1567
3726
  });
1568
3727
  continue;
@@ -1572,7 +3731,7 @@ async function runScenarios(client, scenarios, options = {}) {
1572
3731
  ...common,
1573
3732
  status: "incomplete",
1574
3733
  durationMs: 0,
1575
- error: truncateError(scenario.incompleteReason ?? "Scenario is incomplete", options)
3734
+ error: truncateError(scenario.incompleteReason ?? "Scenario is incomplete", options, client)
1576
3735
  });
1577
3736
  continue;
1578
3737
  }
@@ -1604,7 +3763,7 @@ async function runScenarios(client, scenarios, options = {}) {
1604
3763
  ...common,
1605
3764
  status: "failed",
1606
3765
  durationMs: performance.now() - startedAt,
1607
- error: truncateError(error instanceof Error ? error.message : String(error), options)
3766
+ error: truncateError(error instanceof Error ? error.message : String(error), options, client)
1608
3767
  });
1609
3768
  if (timedOut) timedOutScenario = scenario.name;
1610
3769
  } finally {
@@ -1612,11 +3771,21 @@ async function runScenarios(client, scenarios, options = {}) {
1612
3771
  }
1613
3772
  }
1614
3773
  } finally {
3774
+ const unobservedRedactionOmissions = Math.max(
3775
+ 0,
3776
+ client.session.getRedactionOmissionCounts().clientEvents - initialClientEventRedactionOmissions - observedRedactionOmissionEvents
3777
+ );
3778
+ droppedObservedEvents += unobservedRedactionOmissions;
3779
+ redactionOmittedObservedEvents += unobservedRedactionOmissions;
1615
3780
  const unattributed = droppedObservedEvents - attributedDroppedObservedEvents;
1616
3781
  const lastResult = results.at(-1);
1617
3782
  if (lastResult && unattributed > 0) {
1618
3783
  lastResult.droppedObservedEvents = (lastResult.droppedObservedEvents ?? 0) + unattributed;
1619
3784
  }
3785
+ const unattributedRedaction = redactionOmittedObservedEvents - attributedRedactionOmittedObservedEvents;
3786
+ if (lastResult && unattributedRedaction > 0) {
3787
+ lastResult.redactionOmittedObservedEvents = (lastResult.redactionOmittedObservedEvents ?? 0) + unattributedRedaction;
3788
+ }
1620
3789
  unsubscribe();
1621
3790
  }
1622
3791
  return results;
@@ -1626,20 +3795,29 @@ function resolveScenarioVariables(scenario, supplied) {
1626
3795
  const required = new Set(scenario.variables ?? []);
1627
3796
  const marker = /\$\{([A-Z][A-Z0-9_]*)\}/g;
1628
3797
  const encodedMarker = /%24%7[Bb]([A-Z][A-Z0-9_]*)%7[Dd]/g;
3798
+ const visitString = (value) => {
3799
+ for (const match of value.matchAll(marker)) required.add(match[1]);
3800
+ for (const match of value.matchAll(encodedMarker)) required.add(match[1]);
3801
+ return value.replace(marker, (original, name) => values[name] ?? original).replace(
3802
+ encodedMarker,
3803
+ (original, name) => values[name] === void 0 ? original : encodeURIComponent(values[name])
3804
+ );
3805
+ };
1629
3806
  const visit = (value) => {
1630
- if (typeof value === "string") {
1631
- for (const match of value.matchAll(marker)) required.add(match[1]);
1632
- for (const match of value.matchAll(encodedMarker)) required.add(match[1]);
1633
- return value.replace(marker, (original, name) => values[name] ?? original).replace(
1634
- encodedMarker,
1635
- (original, name) => values[name] === void 0 ? original : encodeURIComponent(values[name])
1636
- );
1637
- }
3807
+ if (typeof value === "string") return visitString(value);
1638
3808
  if (Array.isArray(value)) return value.map(visit);
1639
3809
  if (value && typeof value === "object") {
1640
- return Object.fromEntries(
1641
- Object.entries(value).map(([key, entry]) => [key, visit(entry)])
1642
- );
3810
+ const entries = [];
3811
+ const keys = /* @__PURE__ */ new Set();
3812
+ for (const [key, entry] of Object.entries(value)) {
3813
+ const resolvedKey = visitString(key);
3814
+ if (keys.has(resolvedKey)) {
3815
+ throw new Error("Scenario variable substitution creates duplicate object key");
3816
+ }
3817
+ keys.add(resolvedKey);
3818
+ entries.push([resolvedKey, visit(entry)]);
3819
+ }
3820
+ return Object.fromEntries(entries);
1643
3821
  }
1644
3822
  return value;
1645
3823
  };
@@ -1656,6 +3834,11 @@ function createMcpFnScenarioReport(results, options = {}) {
1656
3834
  (total, result) => total + (result.droppedObservedEvents ?? 0),
1657
3835
  0
1658
3836
  );
3837
+ const redactionOmittedObservedEvents = results.reduce(
3838
+ (total, result) => total + (result.redactionOmittedObservedEvents ?? 0),
3839
+ 0
3840
+ );
3841
+ const overflowedObservedEvents = droppedObservedEvents - redactionOmittedObservedEvents;
1659
3842
  const report = {
1660
3843
  formatVersion: 1,
1661
3844
  kind: "mcpfn.scenario-report",
@@ -1667,7 +3850,13 @@ function createMcpFnScenarioReport(results, options = {}) {
1667
3850
  incomplete,
1668
3851
  droppedResults: 0,
1669
3852
  droppedObservedEvents,
1670
- ...droppedObservedEvents > 0 ? { incompleteReason: "Observed client events exceeded maxObservedEvents" } : {},
3853
+ ...redactionOmittedObservedEvents > 0 ? { redactionOmittedObservedEvents } : {},
3854
+ ...droppedObservedEvents > 0 ? {
3855
+ incompleteReason: [
3856
+ ...redactionOmittedObservedEvents > 0 ? ["Observed client events were omitted because credential redaction failed"] : [],
3857
+ ...overflowedObservedEvents > 0 ? ["Observed client events exceeded maxObservedEvents"] : []
3858
+ ].join("; ")
3859
+ } : {},
1671
3860
  results: structuredClone(results)
1672
3861
  };
1673
3862
  const maxBytes = options.maxBytes ?? 1048576;
@@ -1808,12 +3997,16 @@ async function executeAuthScenario(scenario, signal, runOptions) {
1808
3997
  }
1809
3998
  assertExpected(await runOptions.auth(scenario, signal), scenario.expect);
1810
3999
  }
1811
- function truncateError(value, options) {
1812
- const redacted = String((0, import_oauth_core2.redactOAuthValue)(value));
4000
+ function truncateError(value, options, client) {
4001
+ let redacted = "Scenario error omitted because credential redaction failed";
4002
+ try {
4003
+ redacted = String(client.session.redact(value));
4004
+ } catch {
4005
+ }
1813
4006
  const maxBytes = options.maxErrorBytes ?? 4096;
1814
- const bytes = new TextEncoder().encode(redacted);
1815
- if (bytes.byteLength <= maxBytes) return redacted;
1816
- return `${new TextDecoder().decode(bytes.slice(0, Math.max(0, maxBytes - 16)))}...[TRUNCATED]`;
4007
+ const bytes2 = new TextEncoder().encode(redacted);
4008
+ if (bytes2.byteLength <= maxBytes) return redacted;
4009
+ return `${new TextDecoder().decode(bytes2.slice(0, Math.max(0, maxBytes - 16)))}...[TRUNCATED]`;
1817
4010
  }
1818
4011
  function defaultSideEffect(operation) {
1819
4012
  if (operation === "tools.call" || operation === "tools.call:task") {
@@ -1877,87 +4070,719 @@ async function listScenarioInventory(client, kind, requestOptions) {
1877
4070
  }
1878
4071
 
1879
4072
  // src/suite.ts
1880
- var import_oauth_core3 = require("@superfunctions/oauth-core");
1881
- async function runMcpFnTargetSuite(options) {
1882
- const timeline = [];
1883
- let droppedTimelineEvents = 0;
1884
- const maxTimelineEvents = options.maxTimelineEvents ?? 500;
1885
- if (!Number.isInteger(maxTimelineEvents) || maxTimelineEvents < 1) {
1886
- throw new Error("maxTimelineEvents must be a positive integer");
4073
+ var import_client5 = require("@mcpfn/client");
4074
+ var suiteCleanupOwners = /* @__PURE__ */ new WeakMap();
4075
+ function retryTargetSuiteCleanup(error) {
4076
+ const owner = suiteCleanupOwners.get(error);
4077
+ if (!owner) return Promise.resolve();
4078
+ if (owner.pending) return owner.pending;
4079
+ const pending = Promise.resolve().then(owner.close).then(
4080
+ () => {
4081
+ suiteCleanupOwners.delete(error);
4082
+ },
4083
+ () => {
4084
+ throw error;
4085
+ }
4086
+ ).finally(() => {
4087
+ owner.pending = void 0;
4088
+ });
4089
+ owner.pending = pending;
4090
+ return pending;
4091
+ }
4092
+ var McpFnTargetSuiteCleanupError = class extends Error {
4093
+ constructor(report, close) {
4094
+ super("Target suite cleanup failed; retain this error and retryCleanup()");
4095
+ this.report = report;
4096
+ this.name = "McpFnTargetSuiteCleanupError";
4097
+ suiteCleanupOwners.set(this, { close });
1887
4098
  }
1888
- const maxReportBytes = options.maxReportBytes ?? 1048576;
1889
- validateReportCap(maxReportBytes);
1890
- const consumerDiagnostic = options.client?.diagnostics;
1891
- const client = await McpFnTestClient.connectTarget(
1892
- options.target,
1893
- options.clientInfo ?? { name: "mcpfn-suite", version: "0.0.1" },
1894
- {
1895
- ...options.client,
1896
- diagnostics: async (event) => {
1897
- timeline.push((0, import_oauth_core3.redactOAuthValue)(event));
1898
- if (timeline.length > maxTimelineEvents) {
1899
- timeline.shift();
1900
- droppedTimelineEvents += 1;
4099
+ report;
4100
+ retryCleanup() {
4101
+ return retryTargetSuiteCleanup(this);
4102
+ }
4103
+ };
4104
+ var McpFnTargetSuiteArtifactCleanupError = class extends Error {
4105
+ constructor(close, cause) {
4106
+ super(`${cause.message}; retain this error and retryCleanup()`, { cause });
4107
+ this.name = "McpFnTargetSuiteArtifactCleanupError";
4108
+ suiteCleanupOwners.set(this, { close });
4109
+ }
4110
+ retryCleanup() {
4111
+ return retryTargetSuiteCleanup(this);
4112
+ }
4113
+ };
4114
+ async function runMcpFnTargetSuite(options) {
4115
+ const artifactRedaction = beginSuiteArtifactRedaction(options.target);
4116
+ let finishRedaction = artifactRedaction.finish;
4117
+ const retainArtifactGuard = (report) => {
4118
+ registerMcpFnTargetSuiteArtifactGuard(
4119
+ report,
4120
+ (artifact) => {
4121
+ if (!artifactRedaction.retained) return false;
4122
+ if (options.target.assertArtifactSafe) {
4123
+ options.target.assertArtifactSafe(artifact);
4124
+ return true;
1901
4125
  }
1902
- await consumerDiagnostic?.(event);
4126
+ if (!options.target.redact) return true;
4127
+ return Object.is(
4128
+ redactSuiteArtifact(options.target, artifact, { preserveKeys: false }),
4129
+ artifact
4130
+ );
4131
+ },
4132
+ finishRedaction,
4133
+ options.maxReportBytes ?? 1048576
4134
+ );
4135
+ finishRedaction = () => void 0;
4136
+ try {
4137
+ preserveMcpFnTargetSuiteArtifact(report, JSON.stringify(report));
4138
+ } catch (error) {
4139
+ disposeMcpFnTargetSuiteArtifactGuard(report);
4140
+ throw error;
4141
+ }
4142
+ };
4143
+ try {
4144
+ const report = await runTargetSuite(options);
4145
+ retainArtifactGuard(report);
4146
+ return report;
4147
+ } catch (error) {
4148
+ if (error instanceof McpFnTargetSuiteCleanupError) {
4149
+ try {
4150
+ retainArtifactGuard(error.report);
4151
+ } catch (artifactError) {
4152
+ const cause = artifactError instanceof Error ? artifactError : new Error("Target report artifact proof failed");
4153
+ throw new McpFnTargetSuiteArtifactCleanupError(
4154
+ () => error.retryCleanup(),
4155
+ cause
4156
+ );
1903
4157
  }
1904
4158
  }
1905
- );
1906
- const execution = await (async () => {
4159
+ throw error;
4160
+ } finally {
4161
+ finishRedaction();
4162
+ }
4163
+ }
4164
+ function beginSuiteArtifactRedaction(target) {
4165
+ let finish;
4166
+ if (target.beginRedactionScope) {
1907
4167
  try {
1908
- if (options.manifest) {
1909
- await assertManifestContract(client, options.manifest, {
1910
- expectedToolNames: options.expectedToolNames
1911
- });
4168
+ finish = target.beginRedactionScope();
4169
+ } catch {
4170
+ return { finish: () => void 0, retained: false };
4171
+ }
4172
+ if (!finish) return { finish: () => void 0, retained: false };
4173
+ } else {
4174
+ finish = beginTargetCredentialRedaction(target);
4175
+ }
4176
+ return {
4177
+ finish: () => {
4178
+ try {
4179
+ finish?.();
4180
+ } catch {
1912
4181
  }
1913
- const results2 = await runScenarios(client, options.scenarios ?? [], options.scenarioRun);
4182
+ },
4183
+ retained: true
4184
+ };
4185
+ }
4186
+ function redactSuiteArtifact(target, value, options = {}) {
4187
+ const scrubbed = target.redact ? target.redact(value, options) : value;
4188
+ return redactTargetCredentials(target, scrubbed, options);
4189
+ }
4190
+ function compareInventoryNames(left, right) {
4191
+ if (left === right) return 0;
4192
+ return left < right ? -1 : 1;
4193
+ }
4194
+ function reportProjectionFailureReason(error, projectionFailure) {
4195
+ if (error instanceof McpFnRedactionLimitError) {
4196
+ return "Credential redaction exceeded its traversal budget";
4197
+ }
4198
+ if (projectionFailure === "redaction") {
4199
+ return "Credential redaction failed; report content omitted because safe serialization failed";
4200
+ }
4201
+ return "Report content omitted because safe serialization failed";
4202
+ }
4203
+ var SuiteDiagnosticCollector = class {
4204
+ constructor(maxEvents, maxBytes, target, consumer) {
4205
+ this.maxEvents = maxEvents;
4206
+ this.maxBytes = maxBytes;
4207
+ this.target = target;
4208
+ this.consumer = consumer;
4209
+ }
4210
+ maxEvents;
4211
+ maxBytes;
4212
+ target;
4213
+ consumer;
4214
+ timeline = [];
4215
+ timelineSizes = [];
4216
+ timelineBytes = 0;
4217
+ observedRedactionOmissions = 0;
4218
+ initialRedactionOmissions = 0;
4219
+ dropped = 0;
4220
+ countExceeded = false;
4221
+ bytesExceeded = false;
4222
+ serializationFailed = false;
4223
+ redactionFailed = false;
4224
+ cleanupFailure;
4225
+ captureBaseline(client) {
4226
+ this.initialRedactionOmissions = client.session.getRedactionOmissionCounts().diagnostics;
4227
+ }
4228
+ captureUnobserved(client) {
4229
+ const total = client?.session.getRedactionOmissionCounts().diagnostics ?? this.initialRedactionOmissions;
4230
+ const unobserved = Math.max(
4231
+ 0,
4232
+ total - this.initialRedactionOmissions - this.observedRedactionOmissions
4233
+ );
4234
+ if (unobserved > 0) {
4235
+ this.redactionFailed = true;
4236
+ this.dropped += unobserved;
4237
+ }
4238
+ }
4239
+ async record(client, event) {
4240
+ if (client?.session.isRedactionOmission(event)) {
4241
+ this.observedRedactionOmissions += 1;
4242
+ this.redactionFailed = true;
4243
+ this.dropped += 1;
4244
+ }
4245
+ if (["token-revocation", "transport-close"].includes(event.phase) && event.outcome === "failed") {
4246
+ this.cleanupFailure ??= normalizeMcpFnReportFailure({
4247
+ name: "CleanupError",
4248
+ message: "Target cleanup failed",
4249
+ code: event.code,
4250
+ phase: event.phase
4251
+ });
4252
+ }
4253
+ const safeEvent = redactTargetCredentials(this.target, event, { preserveKeys: true });
4254
+ const bytes2 = this.serializedBytes(safeEvent);
4255
+ if (bytes2 === void 0) {
4256
+ await this.consumer?.(safeEvent);
4257
+ return;
4258
+ }
4259
+ if (bytes2 > this.maxBytes) {
4260
+ this.bytesExceeded = true;
4261
+ this.dropped += 1;
4262
+ } else {
4263
+ this.timeline.push(safeEvent);
4264
+ this.timelineSizes.push(bytes2);
4265
+ this.timelineBytes += bytes2;
4266
+ this.enforceBounds();
4267
+ }
4268
+ await this.consumer?.(safeEvent);
4269
+ }
4270
+ serializedBytes(event) {
4271
+ try {
4272
+ const json2 = JSON.stringify(event, (_key, value) => {
4273
+ if (value === void 0 || typeof value === "number" && !Number.isFinite(value)) {
4274
+ throw new Error("Diagnostic contains a non-JSON value");
4275
+ }
4276
+ return value;
4277
+ });
4278
+ structuredClone(event);
4279
+ return Buffer.byteLength(json2);
4280
+ } catch {
4281
+ this.serializationFailed = true;
4282
+ this.dropped += 1;
4283
+ return void 0;
4284
+ }
4285
+ }
4286
+ enforceBounds() {
4287
+ while (this.timeline.length > this.maxEvents || this.timelineBytes > this.maxBytes) {
4288
+ this.countExceeded ||= this.timeline.length > this.maxEvents;
4289
+ this.bytesExceeded ||= this.timelineBytes > this.maxBytes;
4290
+ this.timeline.shift();
4291
+ this.timelineBytes -= this.timelineSizes.shift();
4292
+ this.dropped += 1;
4293
+ }
4294
+ }
4295
+ };
4296
+ async function executeConnectedSuite(client, options) {
4297
+ let manifestChecked = false;
4298
+ if (options.manifest) {
4299
+ manifestChecked = true;
4300
+ await assertManifestContract(client, options.manifest, {
4301
+ expectedToolNames: options.expectedToolNames
4302
+ });
4303
+ } else if (options.expectedToolNames) {
4304
+ const actual = (await client.listTools()).map((tool) => tool.name).sort(compareInventoryNames);
4305
+ const expected = [...options.expectedToolNames].sort(compareInventoryNames);
4306
+ if (stableJson(actual) !== stableJson(expected)) {
4307
+ throw new McpFnAssertionError(
4308
+ `Tool inventory mismatch: expected ${stableJson(expected)}, actual ${stableJson(actual)}`
4309
+ );
4310
+ }
4311
+ }
4312
+ const results = await runScenarios(client, options.scenarios ?? [], options.scenarioRun);
4313
+ return {
4314
+ manifestChecked,
4315
+ execution: {
4316
+ results,
4317
+ server: client.session.getServerVersion(),
4318
+ capabilities: client.session.getServerCapabilities()
4319
+ }
4320
+ };
4321
+ }
4322
+ function safeTargetFailure(target, error) {
4323
+ try {
4324
+ return normalizeMcpFnReportFailure(redactSuiteArtifact(target, error, { preserveKeys: true }));
4325
+ } catch {
4326
+ return normalizeMcpFnReportFailure({
4327
+ name: "RedactionError",
4328
+ code: "MCPFN_REDACTION_FAILED",
4329
+ message: "Target failure omitted because credential redaction failed"
4330
+ });
4331
+ }
4332
+ }
4333
+ function initialTargetDescriptor(target) {
4334
+ const kind = target.kind;
4335
+ return ["stdio", "streamable-http", "authenticated-streamable-http", "in-memory", "custom"].includes(kind) ? { kind } : { kind: "custom" };
4336
+ }
4337
+ function safeInitialTargetDescriptor(target) {
4338
+ try {
4339
+ return initialTargetDescriptor(target);
4340
+ } catch {
4341
+ return { kind: "custom" };
4342
+ }
4343
+ }
4344
+ function projectSuiteArtifacts(options, client, execution) {
4345
+ const fallback = {
4346
+ target: safeInitialTargetDescriptor(options.target),
4347
+ execution
4348
+ };
4349
+ try {
4350
+ if (client?.session.state !== "connected") {
4351
+ return execution.results.length || execution.server || execution.capabilities ? { ...fallback, failure: "serialization" } : fallback;
4352
+ }
4353
+ let described;
4354
+ try {
4355
+ described = options.target.describe();
4356
+ } catch {
4357
+ return { ...fallback, failure: "serialization" };
4358
+ }
4359
+ try {
4360
+ const { kind, ...descriptor } = described;
1914
4361
  return {
1915
- results: results2,
1916
- server: client.session.getServerVersion(),
1917
- capabilities: client.session.getServerCapabilities()
4362
+ target: {
4363
+ ...redactSuiteArtifact(options.target, descriptor, { preserveKeys: false }),
4364
+ kind: client.session.preserveArtifactStructure(kind)
4365
+ },
4366
+ manifestHash: options.manifest ? redactSuiteArtifact(options.target, options.manifest.hash, { preserveKeys: false }) : void 0,
4367
+ execution: {
4368
+ server: execution.server === void 0 ? void 0 : redactSuiteArtifact(options.target, execution.server, { preserveKeys: false }),
4369
+ capabilities: execution.capabilities === void 0 ? void 0 : redactSuiteArtifact(options.target, execution.capabilities, { preserveKeys: false }),
4370
+ results: execution.results.map((result) => ({
4371
+ ...result,
4372
+ name: redactSuiteArtifact(options.target, result.name, { preserveKeys: false }),
4373
+ operation: redactSuiteArtifact(options.target, result.operation, { preserveKeys: false }),
4374
+ ...result.tool === void 0 ? {} : {
4375
+ tool: redactSuiteArtifact(options.target, result.tool, { preserveKeys: false })
4376
+ }
4377
+ }))
4378
+ }
1918
4379
  };
1919
- } finally {
1920
- await client.close();
4380
+ } catch {
4381
+ return { ...fallback, failure: "redaction" };
4382
+ }
4383
+ } catch {
4384
+ return { ...fallback, failure: "serialization" };
4385
+ }
4386
+ }
4387
+ var suiteStructureKeys = [
4388
+ "formatVersion",
4389
+ "kind",
4390
+ "artifactValidation",
4391
+ "status",
4392
+ "runtime",
4393
+ "ok",
4394
+ "target",
4395
+ "server",
4396
+ "capabilities",
4397
+ "manifestChecked",
4398
+ "manifestHash",
4399
+ "total",
4400
+ "passed",
4401
+ "failed",
4402
+ "incomplete",
4403
+ "droppedResults",
4404
+ "droppedObservedEvents",
4405
+ "redactionOmittedObservedEvents",
4406
+ "incompleteReason",
4407
+ "failure",
4408
+ "timeline",
4409
+ "droppedTimelineEvents",
4410
+ "results",
4411
+ "node",
4412
+ "scenarioFormatVersion",
4413
+ "reportSchemaVersion",
4414
+ "packages",
4415
+ "testing",
4416
+ "name",
4417
+ "message",
4418
+ "layer",
4419
+ "code",
4420
+ "phase",
4421
+ "details",
4422
+ "outcome",
4423
+ "requestId",
4424
+ "at",
4425
+ "sideEffect",
4426
+ "durationMs",
4427
+ "error",
4428
+ "operation",
4429
+ "tool"
4430
+ ];
4431
+ var suiteStructureValues = [
4432
+ "mcpfn.target-suite-report",
4433
+ "target-aware",
4434
+ "complete",
4435
+ "incomplete",
4436
+ "passed",
4437
+ "failed",
4438
+ "started",
4439
+ "succeeded",
4440
+ "none",
4441
+ "idempotent",
4442
+ "non-idempotent",
4443
+ "mcpfn-preflight",
4444
+ "authorization-server",
4445
+ "resource-server",
4446
+ "mcp-initialization",
4447
+ "scenario",
4448
+ "upstream-conformance",
4449
+ "resource-discovery",
4450
+ "authorization-server-discovery",
4451
+ "client-registration",
4452
+ "authorization-request",
4453
+ "authorization-callback",
4454
+ "token-exchange",
4455
+ "token-refresh",
4456
+ "token-revocation",
4457
+ "transport-connect",
4458
+ "mcp-initialize",
4459
+ "capability-operation",
4460
+ "transport-close",
4461
+ "authenticated-streamable-http",
4462
+ "streamable-http",
4463
+ "stdio",
4464
+ "in-memory",
4465
+ "custom"
4466
+ ];
4467
+ function captureSuiteStructureGuard(target, projection, diagnostics, failure) {
4468
+ const candidates = /* @__PURE__ */ new Set([
4469
+ ...suiteStructureKeys,
4470
+ ...suiteStructureValues,
4471
+ process.version,
4472
+ MCPFN_REPORT_SCHEMA_VERSION,
4473
+ MCPFN_TESTING_VERSION,
4474
+ projection.target.kind
4475
+ ]);
4476
+ if (failure?.phase) candidates.add(failure.phase);
4477
+ for (const result of projection.execution.results) {
4478
+ candidates.add(result.status);
4479
+ if (result.sideEffect) candidates.add(result.sideEffect);
4480
+ }
4481
+ for (const event of diagnostics.timeline) {
4482
+ candidates.add(event.phase);
4483
+ candidates.add(event.outcome);
4484
+ if (event.target?.kind) candidates.add(event.target.kind);
4485
+ }
4486
+ const safe = /* @__PURE__ */ new Set();
4487
+ for (const candidate of candidates) {
4488
+ try {
4489
+ if (redactSuiteArtifact(target, candidate, { preserveKeys: false }) === candidate) {
4490
+ safe.add(candidate);
4491
+ }
4492
+ } catch {
4493
+ }
4494
+ }
4495
+ return {
4496
+ assert(value) {
4497
+ if (!safe.has(value)) throw new McpFnStructuralCredentialCollisionError();
4498
+ }
4499
+ };
4500
+ }
4501
+ function assertStructureKeys(value, guard) {
4502
+ for (const key of Object.keys(value)) guard.assert(key);
4503
+ }
4504
+ function assertTargetStructure(target, guard) {
4505
+ guard.assert("kind");
4506
+ guard.assert(target.kind);
4507
+ }
4508
+ function assertSuiteReportStructure(report, guard) {
4509
+ assertStructureKeys(report, guard);
4510
+ guard.assert(report.kind);
4511
+ if (report.artifactValidation) guard.assert(report.artifactValidation);
4512
+ guard.assert(report.status);
4513
+ assertStructureKeys(report.runtime, guard);
4514
+ guard.assert(report.runtime.node);
4515
+ guard.assert(report.runtime.reportSchemaVersion);
4516
+ assertStructureKeys(report.runtime.packages, guard);
4517
+ guard.assert(report.runtime.packages.testing);
4518
+ assertTargetStructure(report.target, guard);
4519
+ if (report.failure) {
4520
+ assertStructureKeys(report.failure, guard);
4521
+ guard.assert(report.failure.layer);
4522
+ if (report.failure.phase) guard.assert(report.failure.phase);
4523
+ }
4524
+ for (const event of report.timeline) {
4525
+ assertStructureKeys(event, guard);
4526
+ guard.assert(event.phase);
4527
+ guard.assert(event.outcome);
4528
+ if (event.target) assertTargetStructure(event.target, guard);
4529
+ }
4530
+ for (const result of report.results) {
4531
+ assertStructureKeys(result, guard);
4532
+ guard.assert(result.status);
4533
+ if (result.sideEffect) guard.assert(result.sideEffect);
4534
+ }
4535
+ }
4536
+ async function closeSuiteClient(client) {
4537
+ try {
4538
+ await client?.close();
4539
+ return {};
4540
+ } catch (error) {
4541
+ let retainedCleanup;
4542
+ if (error instanceof McpFnTestClientCleanupError) {
4543
+ retainedCleanup = () => error.retryCleanup();
4544
+ } else if (client) {
4545
+ retainedCleanup = () => client.close();
1921
4546
  }
1922
- })();
1923
- const results = execution.results;
4547
+ const terminal = error instanceof McpFnTestClientCleanupError && error.cause !== void 0 ? error.cause : error;
4548
+ const phase = terminal && typeof terminal === "object" && terminal.phase === "token-revocation" ? "token-revocation" : "transport-close";
4549
+ return {
4550
+ retainedCleanup,
4551
+ cleanupFailure: normalizeMcpFnReportFailure({
4552
+ name: "CleanupError",
4553
+ message: "Target cleanup failed",
4554
+ code: "MCPFN_TARGET_CLEANUP_FAILED",
4555
+ phase
4556
+ })
4557
+ };
4558
+ }
4559
+ }
4560
+ async function connectSuite(options, diagnostics) {
4561
+ let client;
4562
+ let initialClientEventRedactionOmissions = 0;
4563
+ let manifestChecked = false;
4564
+ let failure;
4565
+ let execution = { results: [] };
4566
+ try {
4567
+ client = McpFnTestClient.createTarget(
4568
+ options.target,
4569
+ options.clientInfo ?? { name: "mcpfn-suite", version: "0.0.1" },
4570
+ {
4571
+ ...options.client,
4572
+ diagnostics: (event) => diagnostics.record(client, event)
4573
+ }
4574
+ );
4575
+ initialClientEventRedactionOmissions = client.session.getRedactionOmissionCounts().clientEvents;
4576
+ diagnostics.captureBaseline(client);
4577
+ await client.session.connect();
4578
+ ({ execution, manifestChecked } = await executeConnectedSuite(client, options));
4579
+ } catch (error) {
4580
+ failure = safeTargetFailure(options.target, error);
4581
+ }
4582
+ return {
4583
+ client,
4584
+ initialClientEventRedactionOmissions,
4585
+ manifestChecked,
4586
+ failure,
4587
+ execution
4588
+ };
4589
+ }
4590
+ async function projectAndCloseSuite(options, diagnostics, connection) {
4591
+ let projection;
4592
+ let structureGuard;
4593
+ let closeResult = {};
4594
+ try {
4595
+ projection = projectSuiteArtifacts(options, connection.client, connection.execution);
4596
+ structureGuard = captureSuiteStructureGuard(
4597
+ options.target,
4598
+ projection,
4599
+ diagnostics,
4600
+ connection.failure
4601
+ );
4602
+ } finally {
4603
+ closeResult = await closeSuiteClient(connection.client);
4604
+ }
4605
+ return { projection, structureGuard, closeResult };
4606
+ }
4607
+ function suiteIncompleteReasons(failure, cleanupFailure, diagnostics, redactionOmittedObservedEvents, overflowedObservedEvents) {
4608
+ const reasons = [
4609
+ ...failure ? [`${failure.layer}: ${failure.message}`] : [],
4610
+ ...cleanupFailure ? [`Cleanup: ${cleanupFailure.message}`] : [],
4611
+ ...diagnostics.serializationFailed ? ["Diagnostic timeline contained non-JSON data"] : [],
4612
+ ...diagnostics.redactionFailed ? ["Diagnostic timeline redaction failed; original events omitted"] : [],
4613
+ ...diagnostics.countExceeded ? ["Diagnostic timeline exceeded maxTimelineEvents"] : [],
4614
+ ...diagnostics.bytesExceeded ? ["Diagnostic timeline exceeded maxReportBytes"] : [],
4615
+ ...redactionOmittedObservedEvents > 0 ? ["Observed client events were omitted because credential redaction failed"] : [],
4616
+ ...overflowedObservedEvents > 0 ? ["Observed client events exceeded maxObservedEvents"] : []
4617
+ ];
4618
+ return reasons.length ? reasons.join("; ") : void 0;
4619
+ }
4620
+ function buildSuiteReport(projection, manifestChecked, failure, cleanupFailure, diagnostics, lifecycleRedactionOmissions) {
4621
+ const results = projection.execution.results;
1924
4622
  const failed = results.filter((result) => result.status === "failed").length;
1925
4623
  const incomplete = results.filter((result) => result.status === "incomplete").length;
1926
- const droppedObservedEvents = results.reduce(
4624
+ const scenarioDroppedObservedEvents = results.reduce(
1927
4625
  (total, result) => total + (result.droppedObservedEvents ?? 0),
1928
4626
  0
1929
4627
  );
1930
- const artifactIncomplete = incomplete > 0 || droppedTimelineEvents > 0 || droppedObservedEvents > 0;
1931
- const report = {
4628
+ const scenarioRedactionOmissions = results.reduce(
4629
+ (total, result) => total + (result.redactionOmittedObservedEvents ?? 0),
4630
+ 0
4631
+ );
4632
+ const redactionOmittedObservedEvents = Math.max(
4633
+ lifecycleRedactionOmissions,
4634
+ scenarioRedactionOmissions
4635
+ );
4636
+ const overflowedObservedEvents = Math.max(
4637
+ 0,
4638
+ scenarioDroppedObservedEvents - scenarioRedactionOmissions
4639
+ );
4640
+ const droppedObservedEvents = redactionOmittedObservedEvents + overflowedObservedEvents;
4641
+ const artifactIncomplete = Boolean(failure) || incomplete > 0 || diagnostics.dropped > 0 || droppedObservedEvents > 0;
4642
+ const incompleteReason = suiteIncompleteReasons(
4643
+ failure,
4644
+ cleanupFailure,
4645
+ diagnostics,
4646
+ redactionOmittedObservedEvents,
4647
+ overflowedObservedEvents
4648
+ );
4649
+ return {
1932
4650
  formatVersion: 1,
1933
4651
  kind: "mcpfn.target-suite-report",
4652
+ artifactValidation: "target-aware",
1934
4653
  status: artifactIncomplete ? "incomplete" : "complete",
1935
- runtime: { node: process.version, scenarioFormatVersion: 1 },
4654
+ runtime: {
4655
+ node: process.version,
4656
+ scenarioFormatVersion: 1,
4657
+ reportSchemaVersion: MCPFN_REPORT_SCHEMA_VERSION,
4658
+ packages: { testing: MCPFN_TESTING_VERSION }
4659
+ },
1936
4660
  ok: failed === 0 && !artifactIncomplete,
1937
- target: (0, import_oauth_core3.redactOAuthValue)(
1938
- options.target.describe()
1939
- ),
1940
- server: execution.server,
1941
- capabilities: execution.capabilities,
1942
- manifestChecked: Boolean(options.manifest),
1943
- ...options.manifest ? { manifestHash: options.manifest.hash } : {},
4661
+ target: projection.target,
4662
+ server: projection.execution.server,
4663
+ capabilities: projection.execution.capabilities,
4664
+ manifestChecked,
4665
+ ...projection.manifestHash === void 0 ? {} : { manifestHash: projection.manifestHash },
1944
4666
  total: results.length,
1945
4667
  passed: results.length - failed - incomplete,
1946
4668
  failed,
1947
4669
  incomplete,
1948
4670
  droppedResults: 0,
1949
4671
  droppedObservedEvents,
1950
- ...droppedTimelineEvents > 0 || droppedObservedEvents > 0 ? {
1951
- incompleteReason: [
1952
- ...droppedTimelineEvents > 0 ? ["Diagnostic timeline exceeded maxTimelineEvents"] : [],
1953
- ...droppedObservedEvents > 0 ? ["Observed client events exceeded maxObservedEvents"] : []
1954
- ].join("; ")
1955
- } : {},
1956
- timeline,
1957
- droppedTimelineEvents,
4672
+ ...redactionOmittedObservedEvents > 0 ? { redactionOmittedObservedEvents } : {},
4673
+ ...incompleteReason ? { incompleteReason } : {},
4674
+ ...failure ? { failure } : {},
4675
+ timeline: diagnostics.timeline,
4676
+ droppedTimelineEvents: diagnostics.dropped,
1958
4677
  results
1959
4678
  };
1960
- return enforceReportCap(report, maxReportBytes);
4679
+ }
4680
+ function structuralReportFailure(retainedCleanup) {
4681
+ const failure = new import_client5.McpFnClientError(
4682
+ "MCPFN_OPERATION_FAILED",
4683
+ "Target report cannot be serialized because a credential conflicts with required artifact structure",
4684
+ { phase: "capability-operation" }
4685
+ );
4686
+ if (retainedCleanup) {
4687
+ throw new McpFnTargetSuiteArtifactCleanupError(retainedCleanup, failure);
4688
+ }
4689
+ throw failure;
4690
+ }
4691
+ function boundedSuiteReport(report, guard, maxReportBytes) {
4692
+ const bounded = enforceReportCap(report, maxReportBytes);
4693
+ assertSuiteReportStructure(bounded, guard);
4694
+ return bounded;
4695
+ }
4696
+ function finalizeSuiteReport(options, projection, report, guard, maxReportBytes, retainedCleanup) {
4697
+ let finalized;
4698
+ try {
4699
+ if (projection.failure) throw new Error("Report payload projection failed");
4700
+ assertSuiteReportStructure(report, guard);
4701
+ finalized = boundedSuiteReport(
4702
+ redactTargetCredentials(options.target, report, { preserveKeys: true }),
4703
+ guard,
4704
+ maxReportBytes
4705
+ );
4706
+ } catch (error) {
4707
+ if (error instanceof McpFnStructuralCredentialCollisionError) {
4708
+ return structuralReportFailure(retainedCleanup);
4709
+ }
4710
+ const fallback = {
4711
+ ...report,
4712
+ ok: false,
4713
+ status: "incomplete",
4714
+ incompleteReason: reportProjectionFailureReason(error, projection.failure),
4715
+ target: { kind: "custom" },
4716
+ server: void 0,
4717
+ capabilities: void 0,
4718
+ manifestHash: void 0,
4719
+ failure: void 0,
4720
+ results: [],
4721
+ timeline: [],
4722
+ droppedResults: report.results.length,
4723
+ droppedTimelineEvents: report.droppedTimelineEvents + report.timeline.length
4724
+ };
4725
+ try {
4726
+ finalized = boundedSuiteReport(
4727
+ redactTargetCredentials(options.target, fallback, { preserveKeys: true }),
4728
+ guard,
4729
+ maxReportBytes
4730
+ );
4731
+ } catch (fallbackError) {
4732
+ if (fallbackError instanceof McpFnRedactionLimitError) {
4733
+ return structuralReportFailure(retainedCleanup);
4734
+ }
4735
+ throw fallbackError;
4736
+ }
4737
+ }
4738
+ if (retainedCleanup) throw new McpFnTargetSuiteCleanupError(finalized, retainedCleanup);
4739
+ return finalized;
4740
+ }
4741
+ async function runTargetSuite(options) {
4742
+ const maxTimelineEvents = options.maxTimelineEvents ?? 500;
4743
+ if (!Number.isInteger(maxTimelineEvents) || maxTimelineEvents < 1) {
4744
+ throw new Error("maxTimelineEvents must be a positive integer");
4745
+ }
4746
+ const maxReportBytes = options.maxReportBytes ?? 1048576;
4747
+ validateReportCap(maxReportBytes);
4748
+ const consumerDiagnostic = options.client?.diagnostics;
4749
+ const diagnostics = new SuiteDiagnosticCollector(
4750
+ maxTimelineEvents,
4751
+ maxReportBytes,
4752
+ options.target,
4753
+ consumerDiagnostic
4754
+ );
4755
+ const connection = await connectSuite(options, diagnostics);
4756
+ const { projection, structureGuard, closeResult } = await projectAndCloseSuite(
4757
+ options,
4758
+ diagnostics,
4759
+ connection
4760
+ );
4761
+ const { cleanupFailure: closeFailure, retainedCleanup } = closeResult;
4762
+ diagnostics.captureUnobserved(connection.client);
4763
+ const lifecycleRedactionOmissions = Math.max(
4764
+ 0,
4765
+ (connection.client?.session.getRedactionOmissionCounts().clientEvents ?? connection.initialClientEventRedactionOmissions) - connection.initialClientEventRedactionOmissions
4766
+ );
4767
+ let cleanupFailure = closeFailure;
4768
+ cleanupFailure ??= diagnostics.cleanupFailure;
4769
+ const failure = connection.failure ?? cleanupFailure;
4770
+ const report = buildSuiteReport(
4771
+ projection,
4772
+ connection.manifestChecked,
4773
+ failure,
4774
+ cleanupFailure,
4775
+ diagnostics,
4776
+ lifecycleRedactionOmissions
4777
+ );
4778
+ return finalizeSuiteReport(
4779
+ options,
4780
+ projection,
4781
+ report,
4782
+ structureGuard,
4783
+ maxReportBytes,
4784
+ retainedCleanup
4785
+ );
1961
4786
  }
1962
4787
  function enforceReportCap(report, maxBytes) {
1963
4788
  validateReportCap(maxBytes);
@@ -1977,6 +4802,10 @@ function enforceReportCap(report, maxBytes) {
1977
4802
  bounded.capabilities = void 0;
1978
4803
  bounded.timeline = [];
1979
4804
  bounded.droppedTimelineEvents += report.timeline.length;
4805
+ if (bounded.failure) {
4806
+ bounded.failure.details = void 0;
4807
+ bounded.failure.message = "Target operation failed; details omitted to fit report budget";
4808
+ }
1980
4809
  }
1981
4810
  if (bounded.droppedResults > 0 || jsonBytes(bounded) > maxBytes) {
1982
4811
  bounded.ok = false;
@@ -2001,36 +4830,65 @@ function jsonBytes(value) {
2001
4830
  MCPFN_HOST_PROFILES,
2002
4831
  MCPFN_NAMED_OAUTH_HOST_FIXTURES,
2003
4832
  MCPFN_OAUTH_EXTENSION_GRANTS,
4833
+ MCPFN_REPORT_SCHEMA_VERSION,
4834
+ MCPFN_TESTING_VERSION,
2004
4835
  McpFnAssertionError,
2005
4836
  McpFnAuthAssertionError,
4837
+ McpFnClientProfileContractCleanupError,
4838
+ McpFnConformanceCleanupError,
2006
4839
  McpFnMockOAuthAuthorizationServer,
4840
+ McpFnRedactionLimitError,
4841
+ McpFnStructuralCredentialCollisionError,
4842
+ McpFnTargetSuiteArtifactCleanupError,
4843
+ McpFnTargetSuiteCleanupError,
2007
4844
  McpFnTestClient,
4845
+ McpFnTestClientCleanupError,
2008
4846
  OFFICIAL_CONFORMANCE_VERSION,
4847
+ acquireRemoteCredential,
2009
4848
  apiKeyCredential,
2010
4849
  assertAuthRegressionSuite,
2011
4850
  assertAuthorizationCodeClientMetadata,
2012
4851
  assertBearerResourceChallenge,
2013
4852
  assertManifestContract,
2014
4853
  assertStructuredTextParity,
4854
+ assertTargetArtifactSafe,
4855
+ authenticatedHttpTarget,
2015
4856
  bearerCredential,
4857
+ beginTargetCredentialRedaction,
2016
4858
  buildOfficialConformanceArgs,
4859
+ buildOfficialConformanceEnvironment,
2017
4860
  checkHostCompatibility,
2018
4861
  createAuthenticatedConformanceProxy,
2019
4862
  createFetchAuthTarget,
4863
+ createHostedAuthorizationFixtures,
4864
+ createMcpFnClientProfileSnapshot,
2020
4865
  createMcpFnScenario,
2021
4866
  createMcpFnScenarioArtifact,
2022
4867
  createMcpFnScenarioReport,
4868
+ createMcpFnTargetSuiteJUnit,
2023
4869
  createOAuthClientMetadataFixture,
2024
4870
  createOAuthClientMetadataVariants,
2025
4871
  createPkcePair,
4872
+ diffMcpFnClientProfileSnapshots,
4873
+ disposeMcpFnTargetSuiteReport,
4874
+ normalizeMcpFnReportFailure,
2026
4875
  parseBearerChallenge,
4876
+ redactRemoteCredential,
4877
+ redactTargetCredentials,
2027
4878
  runAuthRegressionSuite,
2028
4879
  runAuthenticatedOfficialConformance,
4880
+ runHostedAuthorizationRegression,
4881
+ runMcpFnClientProfileContracts,
2029
4882
  runMcpFnTargetSuite,
2030
4883
  runOfficialConformance,
2031
4884
  runScenarios,
4885
+ serializeMcpFnTargetSuiteReport,
2032
4886
  stableJson,
2033
4887
  startMockOAuthAuthorizationServer,
2034
- validateMcpFnScenarios
4888
+ staticRemoteCredentialProvider,
4889
+ validateMcpFnClientProfileSnapshot,
4890
+ validateMcpFnScenarios,
4891
+ validateMcpFnSchemaPortability,
4892
+ validateRemoteCredentialHeaders
2035
4893
  });
2036
4894
  //# sourceMappingURL=index.cjs.map