@mcpfn/testing 0.0.1 → 0.0.3

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.js CHANGED
@@ -404,6 +404,28 @@ var MCPFN_OAUTH_EXTENSION_GRANTS = {
404
404
  deviceCode: "urn:ietf:params:oauth:grant-type:device_code",
405
405
  generic: "urn:example:params:oauth:grant-type:extension"
406
406
  };
407
+ var MCPFN_NAMED_OAUTH_HOST_FIXTURES = {
408
+ chatgpt: {
409
+ id: "chatgpt",
410
+ name: "ChatGPT connector",
411
+ redirectUris: ["https://chatgpt.com/connector_platform_oauth_redirect"],
412
+ responseTypes: ["code"],
413
+ grantTypes: ["authorization_code", "refresh_token"],
414
+ tokenEndpointAuthMethod: "none"
415
+ },
416
+ claude: {
417
+ id: "claude",
418
+ name: "Claude connector",
419
+ redirectUris: ["https://claude.example.com/oauth/callback"],
420
+ responseTypes: ["code"],
421
+ grantTypes: [
422
+ "authorization_code",
423
+ "refresh_token",
424
+ MCPFN_OAUTH_EXTENSION_GRANTS.jwtBearer
425
+ ],
426
+ tokenEndpointAuthMethod: "none"
427
+ }
428
+ };
407
429
  function createOAuthClientMetadataFixture(options) {
408
430
  return {
409
431
  ...options.extraMetadata,
@@ -451,17 +473,15 @@ function assertAuthorizationCodeClientMetadata(metadata, expectedClientId) {
451
473
  }
452
474
 
453
475
  // src/mock-oauth-server.ts
454
- import { createHash, randomBytes, randomUUID } from "crypto";
476
+ import { randomUUID } from "crypto";
455
477
  import { createServer } from "http";
456
- function base64Url(value) {
457
- return value.toString("base64url");
458
- }
478
+ import { derivePkceS256Challenge, generatePkcePair } from "@superfunctions/oauth-core";
459
479
  function createPkcePair() {
460
- const verifier = base64Url(randomBytes(48));
480
+ const pair = generatePkcePair(48);
461
481
  return {
462
- verifier,
463
- challenge: createHash("sha256").update(verifier).digest("base64url"),
464
- method: "S256"
482
+ verifier: pair.codeVerifier,
483
+ challenge: pair.codeChallenge,
484
+ method: pair.codeChallengeMethod
465
485
  };
466
486
  }
467
487
  function uniqueScopes(value) {
@@ -586,7 +606,8 @@ var McpFnMockOAuthAuthorizationServer = class {
586
606
  grant_types_supported: ["authorization_code", "refresh_token"],
587
607
  code_challenge_methods_supported: ["S256"],
588
608
  token_endpoint_auth_methods_supported: ["none"],
589
- scopes_supported: this.scopesSupported
609
+ scopes_supported: this.scopesSupported,
610
+ client_id_metadata_document_supported: true
590
611
  });
591
612
  }
592
613
  if (request.method === "GET" && path2.startsWith("/client-metadata/")) {
@@ -698,7 +719,7 @@ var McpFnMockOAuthAuthorizationServer = class {
698
719
  return oauthError(400, "invalid_grant", "Authorization code binding does not match");
699
720
  }
700
721
  const verifier = formValue(form, "code_verifier");
701
- const challenge = verifier ? createHash("sha256").update(verifier).digest("base64url") : void 0;
722
+ const challenge = verifier ? derivePkceS256Challenge(verifier) : void 0;
702
723
  if (challenge !== record.codeChallenge) {
703
724
  return oauthError(400, "invalid_grant", "PKCE verification failed");
704
725
  }
@@ -779,7 +800,9 @@ async function startMockOAuthAuthorizationServer(options = {}) {
779
800
  const server = createServer(async (req, res) => {
780
801
  try {
781
802
  if (!oauth) throw new Error("Mock OAuth server is not initialized");
782
- await writeResponse(res, await oauth.handle(await toRequest(req, origin)));
803
+ const request = await toRequest(req, origin);
804
+ const custom = await options.handle?.(request.clone(), oauth);
805
+ await writeResponse(res, custom ?? await oauth.handle(request));
783
806
  } catch {
784
807
  res.statusCode = 500;
785
808
  res.setHeader("content-type", "application/json");
@@ -809,123 +832,210 @@ async function startMockOAuthAuthorizationServer(options = {}) {
809
832
  }
810
833
 
811
834
  // src/client.ts
812
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
813
- import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
814
835
  import {
815
- CallToolResultSchema,
816
- CreateTaskResultSchema
817
- } from "@modelcontextprotocol/sdk/types.js";
836
+ createMcpFnClient,
837
+ customTarget
838
+ } from "@mcpfn/client";
839
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
818
840
  var McpFnTestClient = class _McpFnTestClient {
819
- client;
820
- server;
821
- closed = false;
822
- constructor(client, server) {
823
- this.client = client;
824
- this.server = server;
841
+ session;
842
+ constructor(session) {
843
+ this.session = session;
844
+ }
845
+ get client() {
846
+ return this.session.protocol;
825
847
  }
826
848
  static async connect(server, info = { name: "mcpfn-test-client", version: "1.0.0" }, options = {}) {
827
- const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
828
- const client = new Client(info, { capabilities: options.capabilities ?? {} });
829
- await options.configure?.(client);
830
- await server.connect(serverTransport);
831
- await client.connect(clientTransport);
832
- return new _McpFnTestClient(client, server);
833
- }
834
- async listTools() {
835
- const values = [];
836
- let cursor;
837
- do {
838
- const page = await this.client.listTools(cursor ? { cursor } : void 0);
839
- values.push(...page.tools);
840
- cursor = page.nextCursor;
841
- } while (cursor);
842
- return values;
843
- }
844
- async callTool(name, args = {}) {
845
- return await this.client.callTool({ name, arguments: args });
846
- }
847
- async createToolTask(name, args = {}, options = {}) {
848
- return this.client.request(
849
- {
850
- method: "tools/call",
851
- params: { name, arguments: args, task: options }
852
- },
853
- CreateTaskResultSchema
854
- );
849
+ const serverName = server.info?.name ?? "mcpfn-server";
850
+ return this.connectTarget(customTarget({
851
+ kind: "in-memory",
852
+ descriptor: { server: serverName },
853
+ open: async () => {
854
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
855
+ await server.connect(serverTransport);
856
+ return { transport: clientTransport, close: () => server.close() };
857
+ }
858
+ }), info, options);
859
+ }
860
+ static async connectTarget(target, info, options = {}) {
861
+ const session = createMcpFnClient({
862
+ target,
863
+ info: info ?? { name: "mcpfn-test-client", version: "1.0.0" },
864
+ capabilities: options.capabilities,
865
+ handlers: options.handlers,
866
+ events: options.events,
867
+ configure: options.configure,
868
+ diagnostics: options.diagnostics
869
+ });
870
+ try {
871
+ await session.connect();
872
+ return new _McpFnTestClient(session);
873
+ } catch (error) {
874
+ await session.close(true).catch(() => void 0);
875
+ throw error;
876
+ }
855
877
  }
856
- getTask(taskId) {
857
- return this.client.experimental.tasks.getTask(taskId);
878
+ listTools(options) {
879
+ return this.session.tools.listAll(options);
858
880
  }
859
- getTaskResult(taskId) {
860
- return this.client.experimental.tasks.getTaskResult(
861
- taskId,
862
- CallToolResultSchema
863
- );
881
+ callTool(name, args = {}, options) {
882
+ return this.session.tools.call(name, args, options);
883
+ }
884
+ createToolTask(name, args = {}, task = {}, options) {
885
+ return this.session.tools.createTask(name, args, task, options);
886
+ }
887
+ getTask(taskId, options) {
888
+ return this.session.tasks.get(taskId, options);
889
+ }
890
+ getTaskResult(taskId, options) {
891
+ return this.session.tasks.result(taskId, options);
892
+ }
893
+ listTasks(cursor, options) {
894
+ return this.session.tasks.list(cursor, options);
895
+ }
896
+ cancelTask(taskId, options) {
897
+ return this.session.tasks.cancel(taskId, options);
898
+ }
899
+ listResources(options) {
900
+ return this.session.resources.listAll(options);
901
+ }
902
+ listResourceTemplates(options) {
903
+ return this.session.resources.listTemplatesAll(options);
904
+ }
905
+ readResource(uri, options) {
906
+ return this.session.resources.read(uri, options);
907
+ }
908
+ subscribeResource(uri, options) {
909
+ return this.session.resources.subscribe(uri, options);
910
+ }
911
+ unsubscribeResource(uri, options) {
912
+ return this.session.resources.unsubscribe(uri, options);
913
+ }
914
+ listPrompts(options) {
915
+ return this.session.prompts.listAll(options);
916
+ }
917
+ getPrompt(name, args, options) {
918
+ return this.session.prompts.get(name, args, options);
864
919
  }
865
- listTasks(cursor) {
866
- return this.client.experimental.tasks.listTasks(cursor);
867
- }
868
- cancelTask(taskId) {
869
- return this.client.experimental.tasks.cancelTask(taskId);
870
- }
871
- async listResources() {
872
- const values = [];
873
- let cursor;
874
- do {
875
- const page = await this.client.listResources(cursor ? { cursor } : void 0);
876
- values.push(...page.resources);
877
- cursor = page.nextCursor;
878
- } while (cursor);
879
- return values;
880
- }
881
- async listResourceTemplates() {
882
- const values = [];
883
- let cursor;
884
- do {
885
- const page = await this.client.listResourceTemplates(cursor ? { cursor } : void 0);
886
- values.push(...page.resourceTemplates);
887
- cursor = page.nextCursor;
888
- } while (cursor);
889
- return values;
890
- }
891
- readResource(uri) {
892
- return this.client.readResource({ uri });
893
- }
894
- async subscribeResource(uri) {
895
- await this.client.subscribeResource({ uri });
896
- }
897
- async unsubscribeResource(uri) {
898
- await this.client.unsubscribeResource({ uri });
899
- }
900
- async listPrompts() {
901
- const values = [];
902
- let cursor;
903
- do {
904
- const page = await this.client.listPrompts(cursor ? { cursor } : void 0);
905
- values.push(...page.prompts);
906
- cursor = page.nextCursor;
907
- } while (cursor);
908
- return values;
909
- }
910
- getPrompt(name, args) {
911
- return this.client.getPrompt({ name, ...args ? { arguments: args } : {} });
912
- }
913
- complete(params) {
914
- return this.client.complete(params);
915
- }
916
- async close() {
917
- if (this.closed) return;
918
- this.closed = true;
919
- await this.client.close().catch(() => void 0);
920
- await this.server.close().catch(() => void 0);
920
+ complete(params, options) {
921
+ return this.session.prompts.complete(params, options);
922
+ }
923
+ close() {
924
+ return this.session.close();
921
925
  }
922
926
  };
923
927
 
924
928
  // src/conformance.ts
925
929
  import { spawn } from "child_process";
926
930
  import { existsSync } from "fs";
931
+ import {
932
+ createServer as createServer2,
933
+ request as httpRequest
934
+ } from "http";
935
+ import { request as httpsRequest } from "https";
927
936
  import path from "path";
928
937
  var OFFICIAL_CONFORMANCE_VERSION = "0.1.16";
938
+ async function createAuthenticatedConformanceProxy(options) {
939
+ const upstream = new URL(options.url);
940
+ if (!["http:", "https:"].includes(upstream.protocol)) {
941
+ throw new TypeError(
942
+ "Authenticated conformance upstream must use HTTP or HTTPS"
943
+ );
944
+ }
945
+ if (upstream.username || upstream.password || upstream.hash) {
946
+ throw new TypeError(
947
+ "Authenticated conformance upstream must not contain userinfo or a fragment"
948
+ );
949
+ }
950
+ const hostname = normalizeLoopbackHostname(upstream.hostname);
951
+ if (!hostname) {
952
+ throw new TypeError(
953
+ "Authenticated conformance upstream must use a literal loopback address"
954
+ );
955
+ }
956
+ const protocol = upstream.protocol === "https:" ? "https:" : "http:";
957
+ const port = upstream.port === "" ? void 0 : Number(upstream.port);
958
+ const requestPath = `${upstream.pathname}${upstream.search}`;
959
+ const injected = new Headers(options.headers);
960
+ const activeRequests = /* @__PURE__ */ new Set();
961
+ const activeSockets = /* @__PURE__ */ new Set();
962
+ let proxyAuthority;
963
+ const server = createServer2((incoming, outgoing) => {
964
+ if (!incoming.url?.startsWith("/")) {
965
+ outgoing.writeHead(400).end();
966
+ return;
967
+ }
968
+ const headers = { ...incoming.headers };
969
+ delete headers.connection;
970
+ const usesProxyAuthority = incoming.headers.host === proxyAuthority;
971
+ injected.forEach((value, name) => {
972
+ const normalizedName = name.toLowerCase();
973
+ delete headers[normalizedName];
974
+ if (usesProxyAuthority) headers[normalizedName] = value;
975
+ });
976
+ if (usesProxyAuthority) headers.host = upstream.host;
977
+ const transport = protocol === "https:" ? httpsRequest : httpRequest;
978
+ const proxied = transport(
979
+ {
980
+ protocol,
981
+ hostname,
982
+ port,
983
+ path: requestPath,
984
+ method: incoming.method,
985
+ headers
986
+ },
987
+ (response) => {
988
+ outgoing.writeHead(response.statusCode ?? 502, response.headers);
989
+ response.pipe(outgoing);
990
+ }
991
+ );
992
+ activeRequests.add(proxied);
993
+ proxied.once("close", () => activeRequests.delete(proxied));
994
+ proxied.once("error", () => {
995
+ if (!outgoing.headersSent) outgoing.writeHead(502);
996
+ outgoing.end();
997
+ });
998
+ incoming.once("aborted", () => proxied.destroy());
999
+ incoming.pipe(proxied);
1000
+ });
1001
+ server.on("connection", (socket) => {
1002
+ activeSockets.add(socket);
1003
+ socket.once("close", () => activeSockets.delete(socket));
1004
+ });
1005
+ await new Promise((resolve, reject) => {
1006
+ server.once("error", reject);
1007
+ server.listen(0, "127.0.0.1", () => {
1008
+ server.off("error", reject);
1009
+ resolve();
1010
+ });
1011
+ });
1012
+ const address = server.address();
1013
+ if (!address || typeof address === "string") {
1014
+ await new Promise((resolve) => server.close(() => resolve()));
1015
+ throw new Error("Authenticated conformance proxy did not bind a TCP port");
1016
+ }
1017
+ const url = new URL(
1018
+ upstream.pathname + upstream.search,
1019
+ `http://127.0.0.1:${address.port}`
1020
+ );
1021
+ proxyAuthority = url.host;
1022
+ return {
1023
+ url: url.toString(),
1024
+ close: async () => {
1025
+ for (const request of activeRequests) request.destroy();
1026
+ for (const socket of activeSockets) socket.destroy();
1027
+ if (!server.listening) return;
1028
+ await new Promise((resolve, reject) => {
1029
+ server.close((error) => error ? reject(error) : resolve());
1030
+ });
1031
+ }
1032
+ };
1033
+ }
1034
+ function normalizeLoopbackHostname(hostname) {
1035
+ if (hostname === "127.0.0.1") return "127.0.0.1";
1036
+ if (hostname === "[::1]") return "::1";
1037
+ return void 0;
1038
+ }
929
1039
  function npxInvocation(args) {
930
1040
  if (process.platform !== "win32") return { command: "npx", args };
931
1041
  const npmExecPath = process.env.npm_execpath;
@@ -990,6 +1100,18 @@ async function runOfficialConformance(options) {
990
1100
  });
991
1101
  });
992
1102
  }
1103
+ async function runAuthenticatedOfficialConformance(options) {
1104
+ const { headers, ...conformance } = options;
1105
+ const proxy = await createAuthenticatedConformanceProxy({
1106
+ url: conformance.url,
1107
+ headers
1108
+ });
1109
+ try {
1110
+ return await runOfficialConformance({ ...conformance, url: proxy.url });
1111
+ } finally {
1112
+ await proxy.close();
1113
+ }
1114
+ }
993
1115
 
994
1116
  // src/host-profiles.ts
995
1117
  import {
@@ -1089,59 +1211,733 @@ function checkHostCompatibility(manifest, profile) {
1089
1211
  }
1090
1212
  }
1091
1213
  }
1092
- issues.sort((left, right) => left.path.localeCompare(right.path) || left.code.localeCompare(right.code));
1093
- const status = issues.some((issue) => issue.severity === "incompatible") ? "incompatible" : issues.length ? "degraded" : "compatible";
1214
+ issues.sort((left, right) => {
1215
+ if (left.path !== right.path) return left.path < right.path ? -1 : 1;
1216
+ if (left.code < right.code) return -1;
1217
+ if (left.code > right.code) return 1;
1218
+ return 0;
1219
+ });
1220
+ let status = "compatible";
1221
+ if (issues.some((issue) => issue.severity === "incompatible")) {
1222
+ status = "incompatible";
1223
+ } else if (issues.length > 0) {
1224
+ status = "degraded";
1225
+ }
1094
1226
  return { status, compatible: status !== "incompatible", issues };
1095
1227
  }
1096
1228
 
1097
1229
  // src/scenarios.ts
1098
- async function runScenarios(client, scenarios) {
1230
+ import { redactOAuthValue } from "@superfunctions/oauth-core";
1231
+ function createMcpFnScenario(name, operation, result) {
1232
+ switch (operation.kind) {
1233
+ case "tools.call":
1234
+ return createToolCallScenario(name, operation, result);
1235
+ case "tools.call:task":
1236
+ return createToolTaskScenario(name, operation);
1237
+ case "resources.read":
1238
+ return createResourceReadScenario(name, operation, result);
1239
+ case "resources.subscribe":
1240
+ case "resources.unsubscribe":
1241
+ return createResourceLifecycleScenario(name, operation);
1242
+ case "tasks.get":
1243
+ case "tasks.cancel":
1244
+ return createTaskLifecycleScenario(name, operation);
1245
+ case "tasks.result":
1246
+ return createTaskResultScenario(name, operation, result);
1247
+ case "tasks.list":
1248
+ return createTaskListScenario(name, operation);
1249
+ case "prompts.get":
1250
+ return createPromptScenario(name, operation, result);
1251
+ }
1252
+ }
1253
+ function scenarioBase(name, sideEffect) {
1254
+ return { formatVersion: 1, name, sideEffect };
1255
+ }
1256
+ function createToolCallScenario(name, operation, result) {
1257
+ return {
1258
+ ...scenarioBase(name, "non-idempotent"),
1259
+ kind: "tools.call",
1260
+ tool: operation.name,
1261
+ ...operation.arguments ? { arguments: operation.arguments } : {},
1262
+ expect: {
1263
+ isError: Boolean(result.isError),
1264
+ ...result.structuredContent !== void 0 ? { structuredContent: result.structuredContent } : {}
1265
+ }
1266
+ };
1267
+ }
1268
+ function createToolTaskScenario(name, operation) {
1269
+ return {
1270
+ ...scenarioBase(name, "non-idempotent"),
1271
+ kind: operation.kind,
1272
+ tool: operation.name,
1273
+ ...operation.arguments ? { arguments: operation.arguments } : {},
1274
+ ...operation.task ? { task: operation.task } : {}
1275
+ };
1276
+ }
1277
+ function createResourceReadScenario(name, operation, result) {
1278
+ return {
1279
+ ...scenarioBase(name, "none"),
1280
+ kind: operation.kind,
1281
+ uri: operation.uri,
1282
+ expect: result
1283
+ };
1284
+ }
1285
+ function createResourceLifecycleScenario(name, operation) {
1286
+ return {
1287
+ ...scenarioBase(name, "idempotent"),
1288
+ kind: operation.kind,
1289
+ uri: operation.uri
1290
+ };
1291
+ }
1292
+ function createTaskLifecycleScenario(name, operation) {
1293
+ return {
1294
+ ...scenarioBase(name, operation.kind === "tasks.cancel" ? "idempotent" : "none"),
1295
+ kind: operation.kind,
1296
+ taskId: operation.taskId
1297
+ };
1298
+ }
1299
+ function createTaskResultScenario(name, operation, result) {
1300
+ return {
1301
+ ...scenarioBase(name, "none"),
1302
+ kind: operation.kind,
1303
+ taskId: operation.taskId,
1304
+ expect: result
1305
+ };
1306
+ }
1307
+ function createTaskListScenario(name, operation) {
1308
+ return {
1309
+ ...scenarioBase(name, "none"),
1310
+ kind: operation.kind,
1311
+ ...operation.cursor ? { cursor: operation.cursor } : {}
1312
+ };
1313
+ }
1314
+ function createPromptScenario(name, operation, result) {
1315
+ return {
1316
+ ...scenarioBase(name, "none"),
1317
+ kind: operation.kind,
1318
+ prompt: operation.name,
1319
+ ...operation.arguments ? { arguments: operation.arguments } : {},
1320
+ expect: result
1321
+ };
1322
+ }
1323
+ function validateMcpFnScenarios(value) {
1324
+ const artifact = isScenarioArtifact(value) ? value : void 0;
1325
+ if (artifact?.status === "incomplete") {
1326
+ throw new Error(`McpFn scenario artifact is incomplete: ${artifact.incompleteReason}`);
1327
+ }
1328
+ const scenarios = artifact?.scenarios ?? value;
1329
+ if (!Array.isArray(scenarios)) {
1330
+ throw new TypeError("Scenario module must export an array or mcpfn.scenarios artifact");
1331
+ }
1332
+ for (const [index, scenario] of scenarios.entries()) {
1333
+ validateScenarioCandidate(scenario, index);
1334
+ }
1335
+ return scenarios;
1336
+ }
1337
+ var SCENARIO_KINDS_WITHOUT_REQUIRED_FIELDS = /* @__PURE__ */ new Set([
1338
+ "initialize",
1339
+ "tools.list",
1340
+ "resources.list",
1341
+ "resources.templates.list",
1342
+ "prompts.list",
1343
+ "tasks.list",
1344
+ "auth.assert"
1345
+ ]);
1346
+ function validateScenarioCandidate(scenario, index) {
1347
+ if (!scenario || typeof scenario !== "object" || Array.isArray(scenario)) {
1348
+ throw new TypeError(`Invalid scenario at index ${index}`);
1349
+ }
1350
+ const candidate = scenario;
1351
+ validateScenarioIdentity(candidate, index);
1352
+ const kind = scenarioKind(candidate, index);
1353
+ validateScenarioOperation(candidate, kind, index);
1354
+ validateScenarioLimits(candidate, index);
1355
+ if (kind === "events.expect") validateEventScenario(candidate, index);
1356
+ validateScenarioMetadata(candidate, index);
1357
+ if (kind === "auth.assert") validateAuthScenario(candidate, index);
1358
+ }
1359
+ function validateScenarioIdentity(candidate, index) {
1360
+ if (candidate.formatVersion !== void 0 && candidate.formatVersion !== 1) {
1361
+ throw new TypeError(`Invalid scenario at index ${index}: unsupported formatVersion`);
1362
+ }
1363
+ if (typeof candidate.name !== "string" || !candidate.name.trim()) {
1364
+ throw new TypeError(`Invalid scenario at index ${index}: name is required`);
1365
+ }
1366
+ }
1367
+ function scenarioKind(candidate, index) {
1368
+ const kind = candidate.kind ?? "tools.call";
1369
+ if (typeof kind !== "string") {
1370
+ throw new TypeError(`Invalid scenario at index ${index}: kind must be a string`);
1371
+ }
1372
+ return kind;
1373
+ }
1374
+ function validateScenarioOperation(candidate, kind, index) {
1375
+ const requiredField = requiredScenarioField(kind);
1376
+ if (requiredField && typeof candidate[requiredField] !== "string") {
1377
+ throw new TypeError(`Invalid scenario at index ${index}: ${kind} requires ${requiredField}`);
1378
+ }
1379
+ if (!requiredField && !SCENARIO_KINDS_WITHOUT_REQUIRED_FIELDS.has(kind)) {
1380
+ throw new TypeError(`Invalid scenario at index ${index}: unsupported kind ${kind}`);
1381
+ }
1382
+ }
1383
+ function validateScenarioLimits(candidate, index) {
1384
+ if (candidate.timeoutMs !== void 0 && (!Number.isInteger(candidate.timeoutMs) || Number(candidate.timeoutMs) < 1 || Number(candidate.timeoutMs) > 3e5)) {
1385
+ throw new TypeError(`Invalid scenario at index ${index}: timeoutMs must be 1..300000`);
1386
+ }
1387
+ if (candidate.variables !== void 0 && (!Array.isArray(candidate.variables) || candidate.variables.some((entry) => typeof entry !== "string" || !/^[A-Z][A-Z0-9_]*$/.test(entry)))) {
1388
+ throw new TypeError(`Invalid scenario at index ${index}: variables must be environment names`);
1389
+ }
1390
+ }
1391
+ function validateScenarioMetadata(candidate, index) {
1392
+ if (candidate.status !== void 0 && candidate.status !== "complete" && candidate.status !== "incomplete") {
1393
+ throw new TypeError(`Invalid scenario at index ${index}: status must be complete or incomplete`);
1394
+ }
1395
+ if (candidate.status === "incomplete" && typeof candidate.incompleteReason !== "string") {
1396
+ throw new TypeError(`Invalid scenario at index ${index}: incompleteReason is required`);
1397
+ }
1398
+ if (candidate.sideEffect !== void 0 && (typeof candidate.sideEffect !== "string" || !["none", "idempotent", "non-idempotent"].includes(candidate.sideEffect))) {
1399
+ throw new TypeError(`Invalid scenario at index ${index}: sideEffect is unsupported`);
1400
+ }
1401
+ }
1402
+ function validateEventScenario(candidate, index) {
1403
+ if (candidate.minimum !== void 0 && (!Number.isSafeInteger(candidate.minimum) || Number(candidate.minimum) < 1)) {
1404
+ throw new TypeError(
1405
+ `Invalid scenario at index ${index}: events.expect minimum must be a positive integer`
1406
+ );
1407
+ }
1408
+ }
1409
+ function validateAuthScenario(candidate, index) {
1410
+ const expectation = candidate.expect;
1411
+ if (typeof candidate.phase !== "string" || !expectation || typeof expectation.outcome !== "string" || !["allowed", "denied"].includes(expectation.outcome)) {
1412
+ throw new TypeError(`Invalid scenario at index ${index}: auth.assert expectation is required`);
1413
+ }
1414
+ }
1415
+ function createMcpFnScenarioArtifact(scenarios, options) {
1416
+ const artifactOptions = options ?? { status: "complete" };
1417
+ return {
1418
+ formatVersion: 1,
1419
+ kind: "mcpfn.scenarios",
1420
+ ...artifactOptions,
1421
+ scenarios: validateMcpFnScenarios(scenarios)
1422
+ };
1423
+ }
1424
+ function isScenarioArtifact(value) {
1425
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
1426
+ const candidate = value;
1427
+ if (candidate.kind !== "mcpfn.scenarios") return false;
1428
+ if (candidate.formatVersion !== 1) {
1429
+ throw new Error("Unsupported McpFn scenario artifact formatVersion");
1430
+ }
1431
+ if (candidate.status !== "complete" && candidate.status !== "incomplete") {
1432
+ throw new Error("McpFn scenario artifact status must be complete or incomplete");
1433
+ }
1434
+ if (candidate.status === "incomplete" && typeof candidate.incompleteReason !== "string") {
1435
+ throw new Error("Incomplete McpFn scenario artifact requires incompleteReason");
1436
+ }
1437
+ return true;
1438
+ }
1439
+ async function runScenarios(client, scenarios, options = {}) {
1440
+ if (options.defaultTimeoutMs !== void 0 && (!Number.isInteger(options.defaultTimeoutMs) || options.defaultTimeoutMs < 1 || options.defaultTimeoutMs > 3e5)) {
1441
+ throw new Error("defaultTimeoutMs must be an integer from 1 through 300000");
1442
+ }
1443
+ if (options.maxScenarios !== void 0 && (!Number.isInteger(options.maxScenarios) || options.maxScenarios < 1)) {
1444
+ throw new Error("maxScenarios must be a positive integer");
1445
+ }
1446
+ if (options.maxErrorBytes !== void 0 && (!Number.isInteger(options.maxErrorBytes) || options.maxErrorBytes < 64)) {
1447
+ throw new Error("maxErrorBytes must be an integer of at least 64");
1448
+ }
1449
+ if (options.maxObservedEvents !== void 0 && (!Number.isInteger(options.maxObservedEvents) || options.maxObservedEvents < 1)) {
1450
+ throw new Error("maxObservedEvents must be a positive integer");
1451
+ }
1452
+ if (scenarios.length > (options.maxScenarios ?? 1e3)) {
1453
+ throw new Error(`Scenario count exceeds the configured cap of ${options.maxScenarios ?? 1e3}`);
1454
+ }
1099
1455
  const results = [];
1100
- for (const scenario of scenarios) {
1101
- const startedAt = performance.now();
1102
- try {
1103
- const result = await client.callTool(scenario.tool, scenario.arguments);
1104
- if (result.isError && scenario.expect?.isError === void 0) {
1105
- throw new McpFnAssertionError(
1106
- "Tool returned isError=true without an explicit expect.isError=true assertion"
1107
- );
1456
+ const observedEvents = [];
1457
+ const maxObservedEvents = options.maxObservedEvents ?? 500;
1458
+ let droppedObservedEvents = 0;
1459
+ let attributedDroppedObservedEvents = 0;
1460
+ const pushResult = (result) => {
1461
+ const newlyDropped = droppedObservedEvents - attributedDroppedObservedEvents;
1462
+ results.push({
1463
+ ...result,
1464
+ ...newlyDropped > 0 ? { droppedObservedEvents: (result.droppedObservedEvents ?? 0) + newlyDropped } : {}
1465
+ });
1466
+ attributedDroppedObservedEvents = droppedObservedEvents;
1467
+ };
1468
+ const unsubscribe = client.session.onEvent((event) => {
1469
+ observedEvents.push(event);
1470
+ if (observedEvents.length > maxObservedEvents) {
1471
+ observedEvents.shift();
1472
+ droppedObservedEvents += 1;
1473
+ }
1474
+ });
1475
+ let timedOutScenario;
1476
+ try {
1477
+ for (const scenario of scenarios) {
1478
+ const startedAt = performance.now();
1479
+ const operation = scenario.kind ?? "tools.call";
1480
+ const common = {
1481
+ formatVersion: 1,
1482
+ name: scenario.name,
1483
+ operation,
1484
+ ...isToolScenario(scenario) ? { tool: scenario.tool } : {},
1485
+ sideEffect: scenario.sideEffect ?? defaultSideEffect(operation)
1486
+ };
1487
+ if (timedOutScenario) {
1488
+ pushResult({
1489
+ ...common,
1490
+ status: "incomplete",
1491
+ durationMs: 0,
1492
+ error: truncateError(`Skipped after timed-out scenario: ${timedOutScenario}`, options)
1493
+ });
1494
+ continue;
1108
1495
  }
1109
- if (scenario.expect?.isError !== void 0 && Boolean(result.isError) !== scenario.expect.isError) {
1110
- throw new McpFnAssertionError(
1111
- `Expected isError=${scenario.expect.isError}, received ${Boolean(result.isError)}`
1112
- );
1496
+ const resolved = resolveScenarioVariables(scenario, options.variables);
1497
+ if (resolved.missing.length > 0) {
1498
+ pushResult({
1499
+ ...common,
1500
+ status: "incomplete",
1501
+ durationMs: 0,
1502
+ error: truncateError(
1503
+ `Missing scenario variables: ${resolved.missing.join(", ")}`,
1504
+ options
1505
+ )
1506
+ });
1507
+ continue;
1113
1508
  }
1114
- if (scenario.expect && Object.prototype.hasOwnProperty.call(scenario.expect, "structuredContent") && stableJson(result.structuredContent) !== stableJson(scenario.expect.structuredContent)) {
1115
- throw new McpFnAssertionError(
1116
- `Structured result mismatch
1509
+ if (scenario.status === "incomplete") {
1510
+ pushResult({
1511
+ ...common,
1512
+ status: "incomplete",
1513
+ durationMs: 0,
1514
+ error: truncateError(scenario.incompleteReason ?? "Scenario is incomplete", options)
1515
+ });
1516
+ continue;
1517
+ }
1518
+ const timeoutMs = scenario.timeoutMs ?? options.defaultTimeoutMs ?? 3e4;
1519
+ const controller = new AbortController();
1520
+ let timer;
1521
+ let timedOut = false;
1522
+ try {
1523
+ await Promise.race([
1524
+ executeScenario(client, resolved.scenario, {
1525
+ signal: controller.signal,
1526
+ timeout: timeoutMs
1527
+ }, observedEvents, options),
1528
+ new Promise((_resolve, reject) => {
1529
+ timer = setTimeout(() => {
1530
+ timedOut = true;
1531
+ controller.abort();
1532
+ reject(new McpFnAssertionError(`Scenario timed out after ${timeoutMs}ms`));
1533
+ }, timeoutMs);
1534
+ })
1535
+ ]);
1536
+ pushResult({
1537
+ ...common,
1538
+ status: "passed",
1539
+ durationMs: performance.now() - startedAt
1540
+ });
1541
+ } catch (error) {
1542
+ pushResult({
1543
+ ...common,
1544
+ status: "failed",
1545
+ durationMs: performance.now() - startedAt,
1546
+ error: truncateError(error instanceof Error ? error.message : String(error), options)
1547
+ });
1548
+ if (timedOut) timedOutScenario = scenario.name;
1549
+ } finally {
1550
+ if (timer) clearTimeout(timer);
1551
+ }
1552
+ }
1553
+ } finally {
1554
+ const unattributed = droppedObservedEvents - attributedDroppedObservedEvents;
1555
+ const lastResult = results.at(-1);
1556
+ if (lastResult && unattributed > 0) {
1557
+ lastResult.droppedObservedEvents = (lastResult.droppedObservedEvents ?? 0) + unattributed;
1558
+ }
1559
+ unsubscribe();
1560
+ }
1561
+ return results;
1562
+ }
1563
+ function resolveScenarioVariables(scenario, supplied) {
1564
+ const values = { ...process.env, ...supplied };
1565
+ const required = new Set(scenario.variables ?? []);
1566
+ const marker = /\$\{([A-Z][A-Z0-9_]*)\}/g;
1567
+ const encodedMarker = /%24%7[Bb]([A-Z][A-Z0-9_]*)%7[Dd]/g;
1568
+ const visit = (value) => {
1569
+ if (typeof value === "string") {
1570
+ for (const match of value.matchAll(marker)) required.add(match[1]);
1571
+ for (const match of value.matchAll(encodedMarker)) required.add(match[1]);
1572
+ return value.replace(marker, (original, name) => values[name] ?? original).replace(
1573
+ encodedMarker,
1574
+ (original, name) => values[name] === void 0 ? original : encodeURIComponent(values[name])
1575
+ );
1576
+ }
1577
+ if (Array.isArray(value)) return value.map(visit);
1578
+ if (value && typeof value === "object") {
1579
+ return Object.fromEntries(
1580
+ Object.entries(value).map(([key, entry]) => [key, visit(entry)])
1581
+ );
1582
+ }
1583
+ return value;
1584
+ };
1585
+ const resolved = visit(scenario);
1586
+ return {
1587
+ scenario: resolved,
1588
+ missing: [...required].filter((name) => values[name] === void 0).sort(compareCodeUnits)
1589
+ };
1590
+ }
1591
+ function createMcpFnScenarioReport(results, options = {}) {
1592
+ const failed = results.filter((result) => result.status === "failed").length;
1593
+ const incomplete = results.filter((result) => result.status === "incomplete").length;
1594
+ const droppedObservedEvents = results.reduce(
1595
+ (total, result) => total + (result.droppedObservedEvents ?? 0),
1596
+ 0
1597
+ );
1598
+ const report = {
1599
+ formatVersion: 1,
1600
+ kind: "mcpfn.scenario-report",
1601
+ status: incomplete === 0 && droppedObservedEvents === 0 ? "complete" : "incomplete",
1602
+ ...options.manifestHash ? { manifestHash: options.manifestHash } : {},
1603
+ total: results.length,
1604
+ passed: results.length - failed - incomplete,
1605
+ failed,
1606
+ incomplete,
1607
+ droppedResults: 0,
1608
+ droppedObservedEvents,
1609
+ ...droppedObservedEvents > 0 ? { incompleteReason: "Observed client events exceeded maxObservedEvents" } : {},
1610
+ results: structuredClone(results)
1611
+ };
1612
+ const maxBytes = options.maxBytes ?? 1048576;
1613
+ if (!Number.isInteger(maxBytes) || maxBytes < 1024) {
1614
+ throw new Error("maxBytes must be an integer of at least 1024");
1615
+ }
1616
+ if (serializedBytes(report) > maxBytes) {
1617
+ report.status = "incomplete";
1618
+ report.incompleteReason = "Report content exceeded maxBytes and was truncated";
1619
+ }
1620
+ while (serializedBytes(report) > maxBytes && report.results.length > 0) {
1621
+ report.results.pop();
1622
+ report.droppedResults += 1;
1623
+ }
1624
+ if (report.droppedResults > 0) {
1625
+ report.status = "incomplete";
1626
+ }
1627
+ if (serializedBytes(report) > maxBytes) {
1628
+ throw new Error("The minimum scenario report exceeds maxBytes");
1629
+ }
1630
+ return report;
1631
+ }
1632
+ function serializedBytes(value) {
1633
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength;
1634
+ }
1635
+ async function executeScenario(client, scenario, requestOptions, observedEvents, runOptions) {
1636
+ const kind = scenario.kind ?? "tools.call";
1637
+ if (isInventoryScenario(scenario)) {
1638
+ await executeInventoryScenario(client, scenario, requestOptions);
1639
+ return;
1640
+ }
1641
+ switch (scenario.kind) {
1642
+ case void 0:
1643
+ case "tools.call":
1644
+ return executeToolScenario(client, scenario, requestOptions);
1645
+ case "initialize":
1646
+ return executeInitializeScenario(client, scenario);
1647
+ case "resources.read":
1648
+ return assertExpectedAsync(client.readResource(scenario.uri, requestOptions), scenario.expect);
1649
+ case "resources.subscribe":
1650
+ return client.subscribeResource(scenario.uri, requestOptions);
1651
+ case "resources.unsubscribe":
1652
+ return client.unsubscribeResource(scenario.uri, requestOptions);
1653
+ case "prompts.get":
1654
+ return assertExpectedAsync(
1655
+ client.getPrompt(scenario.prompt, scenario.arguments, requestOptions),
1656
+ scenario.expect
1657
+ );
1658
+ case "tools.call:task":
1659
+ return assertExpectedAsync(
1660
+ client.createToolTask(scenario.tool, scenario.arguments, scenario.task, requestOptions),
1661
+ scenario.expect
1662
+ );
1663
+ case "tasks.get":
1664
+ return assertExpectedAsync(client.getTask(scenario.taskId, requestOptions), scenario.expect);
1665
+ case "tasks.result":
1666
+ return assertExpectedAsync(client.getTaskResult(scenario.taskId, requestOptions), scenario.expect);
1667
+ case "tasks.cancel":
1668
+ return assertExpectedAsync(client.cancelTask(scenario.taskId, requestOptions), scenario.expect);
1669
+ case "tasks.list":
1670
+ return assertExpectedAsync(client.listTasks(scenario.cursor, requestOptions), scenario.expect);
1671
+ case "events.expect":
1672
+ return executeEventScenario(scenario, observedEvents);
1673
+ case "auth.assert":
1674
+ return executeAuthScenario(scenario, requestOptions.signal, runOptions);
1675
+ }
1676
+ throw new McpFnAssertionError(`Unsupported scenario operation ${kind}`);
1677
+ }
1678
+ async function executeToolScenario(client, scenario, requestOptions) {
1679
+ const result = await client.callTool(
1680
+ scenario.tool,
1681
+ scenario.arguments,
1682
+ requestOptions
1683
+ );
1684
+ assertToolResult(result, scenario);
1685
+ await scenario.verify?.(result);
1686
+ }
1687
+ function assertToolResult(result, scenario) {
1688
+ if (result.isError && scenario.expect?.isError === void 0) {
1689
+ throw new McpFnAssertionError(
1690
+ "Tool returned isError=true without an explicit expect.isError=true assertion"
1691
+ );
1692
+ }
1693
+ if (scenario.expect?.isError !== void 0 && Boolean(result.isError) !== scenario.expect.isError) {
1694
+ throw new McpFnAssertionError(
1695
+ `Expected isError=${scenario.expect.isError}, received ${Boolean(result.isError)}`
1696
+ );
1697
+ }
1698
+ if (scenario.expect && Object.hasOwn(scenario.expect, "structuredContent") && stableJson(result.structuredContent) !== stableJson(scenario.expect.structuredContent)) {
1699
+ throw new McpFnAssertionError(
1700
+ `Structured result mismatch
1117
1701
  expected: ${stableJson(scenario.expect.structuredContent)}
1118
1702
  actual: ${stableJson(result.structuredContent)}`
1119
- );
1703
+ );
1704
+ }
1705
+ if (scenario.expect?.structuredTextParity) assertStructuredTextParity(result);
1706
+ }
1707
+ function executeInitializeScenario(client, scenario) {
1708
+ const actual = client.client.getServerCapabilities() ?? {};
1709
+ if (scenario.expectCapabilities && stableJson(actual) !== stableJson(scenario.expectCapabilities)) {
1710
+ throw new McpFnAssertionError(
1711
+ `Capability mismatch
1712
+ expected: ${stableJson(scenario.expectCapabilities)}
1713
+ actual: ${stableJson(actual)}`
1714
+ );
1715
+ }
1716
+ }
1717
+ async function executeInventoryScenario(client, scenario, requestOptions) {
1718
+ const values = await listScenarioInventory(client, scenario.kind, requestOptions);
1719
+ if (!scenario.expectNames) return;
1720
+ const actual = values.map((value) => value.name).sort(compareCodeUnits);
1721
+ const expected = [...scenario.expectNames].sort(compareCodeUnits);
1722
+ if (stableJson(actual) !== stableJson(expected)) {
1723
+ throw new McpFnAssertionError(
1724
+ `Inventory mismatch
1725
+ expected: ${stableJson(expected)}
1726
+ actual: ${stableJson(actual)}`
1727
+ );
1728
+ }
1729
+ }
1730
+ async function assertExpectedAsync(actual, expected) {
1731
+ assertExpected(await actual, expected);
1732
+ }
1733
+ function executeEventScenario(scenario, observedEvents) {
1734
+ const matching = observedEvents.filter((event) => event.kind === scenario.event);
1735
+ if (matching.length < (scenario.minimum ?? 1)) {
1736
+ throw new McpFnAssertionError(
1737
+ `Expected at least ${scenario.minimum ?? 1} ${scenario.event} event(s), received ${matching.length}`
1738
+ );
1739
+ }
1740
+ if (scenario.expectPayload !== void 0) {
1741
+ assertExpected(matching.at(-1)?.payload, scenario.expectPayload);
1742
+ }
1743
+ }
1744
+ async function executeAuthScenario(scenario, signal, runOptions) {
1745
+ if (!runOptions.auth) {
1746
+ throw new McpFnAssertionError("auth.assert requires a scenario auth adapter");
1747
+ }
1748
+ assertExpected(await runOptions.auth(scenario, signal), scenario.expect);
1749
+ }
1750
+ function truncateError(value, options) {
1751
+ const redacted = String(redactOAuthValue(value));
1752
+ const maxBytes = options.maxErrorBytes ?? 4096;
1753
+ const bytes = new TextEncoder().encode(redacted);
1754
+ if (bytes.byteLength <= maxBytes) return redacted;
1755
+ return `${new TextDecoder().decode(bytes.slice(0, Math.max(0, maxBytes - 16)))}...[TRUNCATED]`;
1756
+ }
1757
+ function defaultSideEffect(operation) {
1758
+ if (operation === "tools.call" || operation === "tools.call:task") {
1759
+ return "non-idempotent";
1760
+ }
1761
+ if (operation === "tasks.cancel") return "idempotent";
1762
+ return "none";
1763
+ }
1764
+ function compareCodeUnits(left, right) {
1765
+ if (left < right) return -1;
1766
+ if (left > right) return 1;
1767
+ return 0;
1768
+ }
1769
+ function assertExpected(actual, expected) {
1770
+ if (expected !== void 0 && stableJson(actual) !== stableJson(expected)) {
1771
+ throw new McpFnAssertionError(
1772
+ `Scenario result mismatch
1773
+ expected: ${stableJson(expected)}
1774
+ actual: ${stableJson(actual)}`
1775
+ );
1776
+ }
1777
+ }
1778
+ function isToolScenario(scenario) {
1779
+ return "tool" in scenario;
1780
+ }
1781
+ function isInventoryScenario(scenario) {
1782
+ return ["tools.list", "resources.list", "resources.templates.list", "prompts.list"].includes(scenario.kind ?? "");
1783
+ }
1784
+ function requiredScenarioField(kind) {
1785
+ switch (kind) {
1786
+ case "tools.call":
1787
+ case "tools.call:task":
1788
+ return "tool";
1789
+ case "resources.read":
1790
+ case "resources.subscribe":
1791
+ case "resources.unsubscribe":
1792
+ return "uri";
1793
+ case "prompts.get":
1794
+ return "prompt";
1795
+ case "tasks.get":
1796
+ case "tasks.result":
1797
+ case "tasks.cancel":
1798
+ return "taskId";
1799
+ case "events.expect":
1800
+ return "event";
1801
+ default:
1802
+ return void 0;
1803
+ }
1804
+ }
1805
+ async function listScenarioInventory(client, kind, requestOptions) {
1806
+ switch (kind) {
1807
+ case "tools.list":
1808
+ return client.listTools(requestOptions);
1809
+ case "resources.list":
1810
+ return client.listResources(requestOptions);
1811
+ case "resources.templates.list":
1812
+ return client.listResourceTemplates(requestOptions);
1813
+ case "prompts.list":
1814
+ return client.listPrompts(requestOptions);
1815
+ }
1816
+ }
1817
+
1818
+ // src/suite.ts
1819
+ import { redactOAuthValue as redactOAuthValue2 } from "@superfunctions/oauth-core";
1820
+ async function runMcpFnTargetSuite(options) {
1821
+ const timeline = [];
1822
+ let droppedTimelineEvents = 0;
1823
+ const maxTimelineEvents = options.maxTimelineEvents ?? 500;
1824
+ if (!Number.isInteger(maxTimelineEvents) || maxTimelineEvents < 1) {
1825
+ throw new Error("maxTimelineEvents must be a positive integer");
1826
+ }
1827
+ const maxReportBytes = options.maxReportBytes ?? 1048576;
1828
+ validateReportCap(maxReportBytes);
1829
+ const consumerDiagnostic = options.client?.diagnostics;
1830
+ const client = await McpFnTestClient.connectTarget(
1831
+ options.target,
1832
+ options.clientInfo ?? { name: "mcpfn-suite", version: "0.0.1" },
1833
+ {
1834
+ ...options.client,
1835
+ diagnostics: async (event) => {
1836
+ timeline.push(redactOAuthValue2(event));
1837
+ if (timeline.length > maxTimelineEvents) {
1838
+ timeline.shift();
1839
+ droppedTimelineEvents += 1;
1840
+ }
1841
+ await consumerDiagnostic?.(event);
1120
1842
  }
1121
- if (scenario.expect?.structuredTextParity) {
1122
- assertStructuredTextParity(result);
1843
+ }
1844
+ );
1845
+ const execution = await (async () => {
1846
+ try {
1847
+ if (options.manifest) {
1848
+ await assertManifestContract(client, options.manifest, {
1849
+ expectedToolNames: options.expectedToolNames
1850
+ });
1123
1851
  }
1124
- await scenario.verify?.(result);
1125
- results.push({
1126
- name: scenario.name,
1127
- tool: scenario.tool,
1128
- status: "passed",
1129
- durationMs: performance.now() - startedAt
1130
- });
1131
- } catch (error) {
1132
- results.push({
1133
- name: scenario.name,
1134
- tool: scenario.tool,
1135
- status: "failed",
1136
- durationMs: performance.now() - startedAt,
1137
- error: error instanceof Error ? error.message : String(error)
1138
- });
1852
+ const results2 = await runScenarios(client, options.scenarios ?? [], options.scenarioRun);
1853
+ return {
1854
+ results: results2,
1855
+ server: client.session.getServerVersion(),
1856
+ capabilities: client.session.getServerCapabilities()
1857
+ };
1858
+ } finally {
1859
+ await client.close();
1139
1860
  }
1861
+ })();
1862
+ const results = execution.results;
1863
+ const failed = results.filter((result) => result.status === "failed").length;
1864
+ const incomplete = results.filter((result) => result.status === "incomplete").length;
1865
+ const droppedObservedEvents = results.reduce(
1866
+ (total, result) => total + (result.droppedObservedEvents ?? 0),
1867
+ 0
1868
+ );
1869
+ const artifactIncomplete = incomplete > 0 || droppedTimelineEvents > 0 || droppedObservedEvents > 0;
1870
+ const report = {
1871
+ formatVersion: 1,
1872
+ kind: "mcpfn.target-suite-report",
1873
+ status: artifactIncomplete ? "incomplete" : "complete",
1874
+ runtime: { node: process.version, scenarioFormatVersion: 1 },
1875
+ ok: failed === 0 && !artifactIncomplete,
1876
+ target: redactOAuthValue2(
1877
+ options.target.describe()
1878
+ ),
1879
+ server: execution.server,
1880
+ capabilities: execution.capabilities,
1881
+ manifestChecked: Boolean(options.manifest),
1882
+ ...options.manifest ? { manifestHash: options.manifest.hash } : {},
1883
+ total: results.length,
1884
+ passed: results.length - failed - incomplete,
1885
+ failed,
1886
+ incomplete,
1887
+ droppedResults: 0,
1888
+ droppedObservedEvents,
1889
+ ...droppedTimelineEvents > 0 || droppedObservedEvents > 0 ? {
1890
+ incompleteReason: [
1891
+ ...droppedTimelineEvents > 0 ? ["Diagnostic timeline exceeded maxTimelineEvents"] : [],
1892
+ ...droppedObservedEvents > 0 ? ["Observed client events exceeded maxObservedEvents"] : []
1893
+ ].join("; ")
1894
+ } : {},
1895
+ timeline,
1896
+ droppedTimelineEvents,
1897
+ results
1898
+ };
1899
+ return enforceReportCap(report, maxReportBytes);
1900
+ }
1901
+ function enforceReportCap(report, maxBytes) {
1902
+ validateReportCap(maxBytes);
1903
+ const bounded = structuredClone(report);
1904
+ if (jsonBytes(bounded) > maxBytes) {
1905
+ bounded.ok = false;
1906
+ bounded.status = "incomplete";
1907
+ bounded.incompleteReason = "Report content exceeded maxReportBytes and was truncated";
1908
+ }
1909
+ while (jsonBytes(bounded) > maxBytes && bounded.results.length > 0) {
1910
+ bounded.results.pop();
1911
+ bounded.droppedResults += 1;
1912
+ }
1913
+ if (jsonBytes(bounded) > maxBytes) {
1914
+ bounded.target = { kind: report.target.kind };
1915
+ bounded.server = void 0;
1916
+ bounded.capabilities = void 0;
1917
+ bounded.timeline = [];
1918
+ bounded.droppedTimelineEvents += report.timeline.length;
1919
+ }
1920
+ if (bounded.droppedResults > 0 || jsonBytes(bounded) > maxBytes) {
1921
+ bounded.ok = false;
1922
+ bounded.status = "incomplete";
1923
+ bounded.incompleteReason = "Report content exceeded maxReportBytes and was truncated";
1924
+ }
1925
+ if (jsonBytes(bounded) > maxBytes) {
1926
+ throw new Error("The minimum target suite report exceeds maxReportBytes");
1927
+ }
1928
+ return bounded;
1929
+ }
1930
+ function validateReportCap(maxBytes) {
1931
+ if (!Number.isInteger(maxBytes) || maxBytes < 1024) {
1932
+ throw new Error("maxReportBytes must be an integer of at least 1024");
1140
1933
  }
1141
- return results;
1934
+ }
1935
+ function jsonBytes(value) {
1936
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength;
1142
1937
  }
1143
1938
  export {
1144
1939
  MCPFN_HOST_PROFILES,
1940
+ MCPFN_NAMED_OAUTH_HOST_FIXTURES,
1145
1941
  MCPFN_OAUTH_EXTENSION_GRANTS,
1146
1942
  McpFnAssertionError,
1147
1943
  McpFnAuthAssertionError,
@@ -1157,15 +1953,22 @@ export {
1157
1953
  bearerCredential,
1158
1954
  buildOfficialConformanceArgs,
1159
1955
  checkHostCompatibility,
1956
+ createAuthenticatedConformanceProxy,
1160
1957
  createFetchAuthTarget,
1958
+ createMcpFnScenario,
1959
+ createMcpFnScenarioArtifact,
1960
+ createMcpFnScenarioReport,
1161
1961
  createOAuthClientMetadataFixture,
1162
1962
  createOAuthClientMetadataVariants,
1163
1963
  createPkcePair,
1164
1964
  parseBearerChallenge,
1165
1965
  runAuthRegressionSuite,
1966
+ runAuthenticatedOfficialConformance,
1967
+ runMcpFnTargetSuite,
1166
1968
  runOfficialConformance,
1167
1969
  runScenarios,
1168
1970
  stableJson,
1169
- startMockOAuthAuthorizationServer
1971
+ startMockOAuthAuthorizationServer,
1972
+ validateMcpFnScenarios
1170
1973
  };
1171
1974
  //# sourceMappingURL=index.js.map